线程
马士兵老师线程示例代码;concurrent文件为源码
前言
- 什么是线程
- 如何创建一个线程
- 线程同步的概念
synchronized关键字
-
当要保证一个代码块在多线程执行时,使用synchronized关键字加锁来实现,一般的方法可以对成员变量加锁,或者也可以对this自身来加锁。
public void m() { synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } }
-
如果是静态方法时,可以对方法加锁,或者对class加锁
public synchronized static void m() { //这里等同于synchronized(yxxy.c_004.T.class) count--; System.out.println(Thread.currentThread().getName() + " count = " + count); }
public static void mm() { synchronized(T.class) { //对this加锁不行,因为静态方法没有实例对象 count --; } }
-
实际业务如果对写加锁,那么在读的时候如果不加锁可能会出现脏读的现象,应结合具体业务来实现需求。
/** * 对业务写方法加锁 * 对业务读方法不加锁 * 容易产生脏读问题(dirtyRead) */ package yxxy.c_008; import java.util.concurrent.TimeUnit; public class Account { String name; double balance; public synchronized void set(String name, double balance) { this.name = name; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } this.balance = balance; } public /*synchronized*/ double getBalance(String name) { return this.balance; } public static void main(String[] args) { Account a = new Account(); new Thread(()->a.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); } }
-
使用synchronize获得的锁是可以重入的
/** * 两个方法的锁都加在一个对象上,当其中一个同步方法调用另外一个同步方法时,再次申请的时候仍然会得到该对象的锁. * 也就是说synchronized获得的锁是可重入的 * @author mashibing */ package yxxy.c_009; import java.util.concurrent.TimeUnit; public class T { synchronized void m1() { System.out.println("m1 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); } synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); } public static void main(String[] args) { T t = new T(); new Thread(() -> t.m1()).start(); } }
-
当线程抛出异常时,会释放锁,所以在线程中对待异常一定要小心。如果不想释放锁,一定要try catch捕获异常,然后进行操作,比如回滚事务
-
volatile和synchronize的区别
每个线程都有自己的工作内存,每个线程的工作内存都从主内存有一份使用到的变量的拷贝,以此来作为缓冲。每个线程所使用到的变量都会从主内存缓存一份到缓冲区。当一个线程修改了变量的值之后,如果CPU很忙的话,它可能就没时间去主内存reload下最新的数据,所以有可能其他线程不会立即拿到最新的数据。
volatile修饰的变量当被多个线程使用时,会保证此变量对于其他线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量不能做到这一点。
所以下面的代码中,main方法修改
running
的变量,线程才会停止/** * volatile 关键字,使一个变量在多个线程间可见 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道 * 使用volatile关键字,会让所有线程都会读到变量的修改值 * * 在下面的代码中,running是存在于堆内存的t对象中 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去 * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行 * * 使用volatile,将会强制所有线程都去堆内存中读取running的值 * * 可以阅读这篇文章进行更深入的理解 * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html * * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized * @author mashibing */ package yxxy.c_012; import java.util.concurrent.TimeUnit; public class T { /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别 void m() { System.out.println("m start"); while(running) { /* try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }*/ } System.out.println("m end!"); } public static void main(String[] args) { T t = new T(); new Thread(t::m, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } t.running = false; } }
但由于volatile只能保证变量的可见性,但是并不能保证volatile的原子性(比如count++这种非原子性的操作)。而synchronize既可以保证可见性又保证原子性。但synchronize的效率比volatile的低很多。
常见的基本数据类型都有对应的原子操作的对象,如:AtomicInteger、AtomicLong,它们的方法都是原子性的,效率也高得多。
-
采用更细粒度的锁,可以使线程争取时间变短,从而提高效率
-
锁是加在堆内存中的对象上,而不是栈里面的引用。当对象的引用发生改变,锁的对象也会发生变化。
-
java的字符串会有一个常量池,当锁的是字符串并且内容相等,那么会发生诡异的死锁现象
/** * 不要以字符串常量作为锁定对象 * 在下面的例子中,m1和m2其实锁定的是同一个对象 * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”, * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞, * 因为你的程序和你用到的类库不经意间使用了同一把锁 * * jetty * * @author mashibing */ package yxxy.c_018; public class T { String s1 = "Hello"; String s2 = "Hello"; void m1() { synchronized(s1) { } } void m2() { synchronized(s2) { } } }
-
wait线程等待时会释放锁;notify唤醒一个线程但不会释放锁
-
``` /**
- 曾经的面试题:(淘宝?)
- 实现一个容器,提供两个方法,add,size
- 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
- 分析下面这个程序,能完成这个功能吗?
- @author mashibing */ ```
/** * 曾经的面试题:(淘宝?) * 实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束 * * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢? * * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以 * * 阅读下面的程序,并分析输出结果 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出 * 想想这是为什么? * * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行 * 整个通信过程比较繁琐 * * 使用Latch(门闩)替代wait notify来进行通知 * 好处是通信方式简单,同时也可以指定等待时间 * 使用await和countdown方法替代wait和notify * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行 * 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了 * 这时应该考虑countdownlatch/cyclicbarrier/semaphore * @author mashibing */ package yxxy.c_019; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class MyContainer5 { // 添加volatile,使t2能够得到通知 volatile List lists = new ArrayList(); public void add(Object o) { lists.add(o); } public int size() { return lists.size(); } public static void main(String[] args) { MyContainer5 c = new MyContainer5(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { System.out.println("t2启动"); if (c.size() != 5) { try { latch.await(); //也可以指定等待时间 //latch.await(5000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t2 结束"); }, "t2").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e1) { e1.printStackTrace(); } new Thread(() -> { System.out.println("t1启动"); for (int i = 0; i < 10; i++) { c.add(new Object()); System.out.println("add " + i); if (c.size() == 5) { // 打开门闩,让t2得以执行 latch.countDown(); } try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1").start(); } }
多线程知识点列表
- synchronize关键字用来指定对象加锁,锁的对象可以是变量、this对象、T.class。当锁定对象是this时等同于synchronize加在方法上;静态方法锁的对象只能是T.class
- 同步方法中调用其他同步方法是没问题的。当锁对象一样时synchronized获得的锁是可重入的
- 当业务的读写分离时容易出现脏读的出现,如果要避免脏读应该读写都加锁
- 当锁的方法抛出异常后,会释放锁,一定要注意。业务代码可以在捕获异常后回滚数据库
- volatile关键字可以保证各线程使用的变量被一个线程修改后,及时通知其他线程reload主内存的数据来获取最新数据,被修饰的变量在线程之间是可见的透明的
- volatile不能保证修改变量时不会发生的不一致问题,比如++操作并不是原子性的所以会出现不一致的问题,synchronize可以避免。如果变量时基本数据类型时可以考虑使用Atomic***来操作数据,但依然不能保证多个方法连续调用是原子性的
- synchronize优化,同步代码块中语句越少并发效果越好
- 当synchronize锁定o对象,如果o变成另一个对象,锁的对象会发生改变,此时在等待的其他线程会得到同步代码块的执行机会。所以应当避免锁对象的改变
- 使用synchronize关键字时不要使用字符串对象作为锁对象,因为字符串有缓冲池概念
- wait()方法等待时会释放锁,notify()方法唤醒其他线程但是不会释放锁
- 门闩
CountDownLatch latch = new CountDownLatch(1);
,使用latch.countDown();
来把计数器-1,当计数器为0时,latch.await();
后面的代码就会得到执行