文章目录
1. Java线程
1. 线程的应用
- 线程可以使程序实现异步调用,如果调用之后需要等待结果返回才能继续运行就是同步;不需要等待结果的返回就可运行就是异步;比如读取磁盘的操作,如果采用同步的话,就必须等待磁盘数据读取完毕,但是读取磁盘的操作是不需要CPU的,CPU将保持空闲状态,所以说浪费了CPU的利用率。但是线程允许CPU异步执行,在读取磁盘的时候CPU可以运行其它的线程而不需要空等,当磁盘操作完成的时候,CPU在选择下一步的执行;
- 提高运行效率:运行效率的提高是基于多核的,如果只有一个核的话,多线程并不能在真正意义上提高程序的运行效率,只是在不同的任务之间进行切换,而且由于上下文切换,反而会是程序运行的更慢。但是多线程在单核也是有存在的意义的,比如应用于交互式的系统之中,如果一个任务需要的时间特别长的话,对于使用计算机的用户来说响应效果会很差。多线程也可以使用异步加快程序的执行。
- 线程的运行是交替执行的,先后不受我们的控制;
2. 创建线程
-
直接使用Thread进行创建:
public class CreatThread1 { public static void main(String[] args) { final Thread thread = new Thread() { @Override public void run() { System.out.println("Thread1..."); } }; // 启动线程 thread.start(); } }
使用该方式的优点就是方便;
-
使用Runnable配合Thread,该方式把线程的任务(run()方法内的代码)和线程分开了,并且使用Runnable可以更容易地与线程池等高级API配合使用,也脱离了Thread的继承体系,更加的灵活:
public class CreatThread2 { public static void main(String[] args) { final Runnable runnable = () -> System.out.println("Runnable..."); new Thread(runnable).start(); } }
Thread与Runnable的关系,我们将Runnable对象传递给了Thread的构造方法,从源码中可以看到它的构造方法中调用了一个init方法:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } // 进入以上的init方法,可以看到如下的代码 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // ... 该方法就是将Runnable对象赋值给了一个target变量,该变量最终会在Thread自己的run()方法之中被用到 this.target = target; // ... } // Thread的run()方法,如果target不为空的话,就会调用target的run()方法,也就是我们传递给Thread构造方法的Runnable对象 @Override public void run() { if (target != null) { target.run(); } }
-
使用FutureTask和Thread结合创建线程,FutureTask其实是对Runnable的扩展,使用它可以用来获取任务的执行结果,需要接受一个Callable类型的对象,Callable是一个接口,其中call()方法有返回值并且可以抛出异常:
public class CreatThread3 { public static void main(String[] args) throws ExecutionException, InterruptedException { final FutureTask<Integer> integerFutureTask = new FutureTask<>(() -> { System.out.println("running..."); Thread.sleep(2000); return 100; }); new Thread(integerFutureTask).start(); System.out.println(integerFutureTask.get()); } }
3. 各个Java方法的使用
-
start()和run()方法:如果不调用start()方法而直接调用run()方法的话也可以执行run之中的代码,但是执行run方法的就不是new Thread创建出来的线程,而仍然是主线程,并不能得到异步处理的效果;start方法启动线程的时候只是将线程的状态从NEW转变为RUNNABLE,但是决定线程是不是会执行的还是操作系统,该方法不能被多次连续调用:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) { final Thread thread = new Thread() { @Override public void run() { log.debug("running"); } }; System.out.println(thread.getState()); thread.start(); System.out.println(thread.getState()); } } // 输出 /* NEW RUNNABLE 16:55:58.178 c.TestThread [Thread-0] - running */
-
sleep 与 yield:调用 sleep() 会让当前线程从 Running 转换为 Timed Waiting;其它线程可以使用interrupt()方法打断正在睡眠的线程,那么被打断的正在睡眠的线程就会抛出InterruptedException异常;睡眠结束后的线程未必会立刻得到执行,建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep(),它将时间划分为不同的时间单位,有更好的可读性:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) throws InterruptedException { final Thread thread = new Thread(() -> { log.debug("enter sleep..."); try { Thread.sleep(2000); } catch (InterruptedException e) { log.debug("wake up"); e.printStackTrace(); } }); thread.start(); Thread.sleep(2000); log.debug("interrupt"); thread.interrupt(); } }
输出:
调用 yield 会让当前线程从Running 进入 Runnable 就绪状态,然后调度执行其它线程,具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)。此外也可以设置线程优先级,但是Java是用户级线程映射到内核级线程的,所以底层的调度器可以忽略优先级,并不是100%有用。thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
-
join()方法:以下的示例会得到有些意想不到的结果:
@Slf4j(topic = "c.TestThread") public class TestThread { static int r = 0; public static void main(String[] args) { log.debug("开始"); final Thread thread = new Thread(() -> { log.debug("开始"); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } r = 10; log.debug("结束"); }); thread.start(); // thread.join(); log.debug("结果为 : {}", r); log.debug("结束"); } } // 输出 /* 18:47:27.883 c.TestThread [main] - 开始 18:47:27.950 c.TestThread [Thread-0] - 开始 18:47:27.950 c.TestThread [main] - 结果为 : 0 18:47:27.952 c.TestThread [Thread-0] - 结束 18:47:27.952 c.TestThread [main] - 结束 */
因为主线程和新创建的线程几乎是同时运行的,但是新创建的线程睡眠了1ms,主线程并不会停下来等待新创建的线程,所以当主线程读取 r 的时候,r 的值仍然是0;可以使用join方法来得到正确的结果;此时主线程需要同步等待thread线程,当该线程运行结束后就可以得到正确的结果。需要等待多个线程的时候,分别执行每个线程的join方法就可以了,也可以向join之中传递参数来表示线程最多会等待的时间,如果超过等待时间之后等待的线程还没有运行结束,就会停止等待。
-
interrupt()方法:当线程处于阻塞态的时候(也就是调用了sleep,wait或join方法),如果打断了相应的线程,打断标记会被设置为false:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) throws InterruptedException { final Thread thread = new Thread(() -> { log.debug("sleep..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1"); thread.start(); Thread.sleep(1000); log.debug("interrupt"); thread.interrupt(); log.debug("打断标记:{}", thread.isInterrupted()); } } // 输出 /* 20:28:27.007 c.TestThread [t1] - sleep... 20:28:28.009 c.TestThread [main] - interrupt 20:28:28.009 c.TestThread [main] - 打断标记:false java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.zhyn.TestThread.lambda$main$0(TestThread.java:13) at java.lang.Thread.run(Thread.java:748) */
如果打断正常的线程的话打断标记会被置为true,并且正常运行的线程会继续运行,可以通过打断标记来决定是否终止当前正在运行的线程:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) throws InterruptedException { final Thread thread = new Thread(() -> { while (true) { final boolean interrupted = Thread.currentThread().isInterrupted(); if (interrupted) break; } }, "t1"); thread.start(); Thread.sleep(1000); log.debug("interrupt"); thread.interrupt(); } }
-
当想要终止一个线程的时候,一个较好的方式是使用两阶段种植模式,如果直接使用stop方法的话会真正的杀死线程,当杀死线程的时候如果其中锁住了共享资源,那么他被杀死后就没有机会释放锁,其它的线程就没有机会再获取到这个锁,如果使用System.exit(int id)的方式终止线程的话又会停止整个程序;但是两阶段终止方式可以是线程自己选择是否终止自己,如下图所示:
当线程被打断的时候线程会先释放掉应当释放的锁等资源,然后才让自己终止,如果线程睡眠之后被打断无异常的话会继续执行监控记录,但是当线程在睡眠的时候被打断的话就会放生异常,此时设置打断标记为true,之后就可以执行终止线程的操作:@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) throws InterruptedException { Monitor monitor = new Monitor(); monitor.start(); Thread.sleep(3500); monitor.stop(); } } @Slf4j(topic = "c.TestThread") class Monitor { Thread monitor; /** * 启动监控器线程 */ public void start() { //设置线控器线程,用于监控线程状态 monitor = new Thread() { @Override public void run() { //开始不停的监控 while (true) { //判断当前线程是否被打断了 if(Thread.currentThread().isInterrupted()) { log.debug("处理后续任务"); //终止线程执行 break; } log.debug("监控器运行中..."); try { //线程休眠 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); //如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记 Thread.currentThread().interrupt(); } } } }; monitor.start(); } /** * 用于停止监控器线程 */ public void stop() { //打断线程 monitor.interrupt(); } }
输出:
-
sleep,yiled,wait,join 对比:
- sleep,join,yield,interrupted是Thread类中的方法;
- wait/notify是object中的方法;
- sleep 不释放锁、释放cpu;
- join 释放锁、抢占cpu;
- yiled 不释放锁、释放cpu;
- wait 释放锁、释放cpu;
-
park()方法:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) throws InterruptedException { test(); } private static void test() throws InterruptedException { final Thread thread = new Thread(() -> { log.debug("park"); LockSupport.park(); log.debug("unpark"); log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); // 当打断状态为true的时候,再调用park方法的话并不会令线程停止,也就是说此时park方法会失效 // 如果想改变打断状态的话可以使用如下的方法,interrupted()会将打断状态置为false // log.debug("打断状态:{}", Thread.interrupted()); LockSupport.park(); log.debug("unpark"); }, "t1"); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
输出:
-
守护线程:默认情况下,Java进程需要等待所有的线程都运行结束之后才会结束。有一种线程叫做守护线程,只要其它的非守护线程都运行结束了,即使守护线程没有运行完毕,也会强制结束。垃圾回收器线程就是一种守护线程,如果程序停止了,垃圾回收线程也会停止;Tomcat中的Acceptor和Poller线程都是一种守护线程,Tomcat用其进行接收请求和分发请求,所以当Tomcat接收到shutdown命令之后并不会等待以上的两个线程完成当前的请求,而是指直接终止以上的两个线程。
4. 线程的状态
1. 五状态
- 五个状态是从操作系统的层次来讲的:
- 初始状态:仅仅是在语言层面上创建了线程对象,即
Thead thread = new Thead();
,但是还未与操作系统内核线程关联; - 可运行状态:也称就绪状态,指该线程已经被创建,与操作系统内核线程相关联,等待CPU给它分配时间片就可运行;
- 运行状态:指线程获取了CPU时间片,正在运行;当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,该过程会导致线程上下文切换;
- 阻塞状态:如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】,等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】,与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片;
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态;
- 初始状态:仅仅是在语言层面上创建了线程对象,即
2. 六状态
-
六状态是根据Java API层次来描述的,Java一共定义了六种状态;这些状态是针对于Java的用户空间的线程,和OS内核空间的线程是由差别的,Java线程和内核线程是一对一的,Java在State枚举类中定义了以下的六种状态:
- NEW (新建状态) 线程刚被创建,但是还没有调用 start() 方法;
- RUNNABLE (运行状态) 当调用了 start() 方法之后,注意,Java API 层面的RUNNABLE 状态涵盖了操作系统层面的 【就绪状态】、【运行中状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行);
- BLOCKED (阻塞状态) , WAITING (等待状态) , TIMED_WAITING(定时等待状态) 都是 Java API 层面对【阻塞状态】的细分;
- TERMINATED (结束状态) 当线程代码运行结束;
@Slf4j(topic = "c.TestState") public class TestState { public static void main(String[] args) { Thread t1 = new Thread("t1") { // new @Override public void run() { log.debug("running..."); } }; Thread t2 = new Thread("t2") { @Override public void run() { while(true) { // runnable } } }; t2.start(); Thread t3 = new Thread("t3") { // TERMINATED @Override public void run() { log.debug("running..."); } }; t3.start(); Thread t4 = new Thread("t4") { @Override public void run() { synchronized (TestState.class) { try { Thread.sleep(1000000); // timed_waiting } catch (InterruptedException e) { e.printStackTrace(); } } } }; t4.start(); Thread t5 = new Thread("t5") { @Override public void run() { try { t2.join(); // waiting } catch (InterruptedException e) { e.printStackTrace(); } } }; t5.start(); Thread t6 = new Thread("t6") { @Override public void run() { synchronized (TestState.class) { // blocked try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t6.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("t1 state {}", t1.getState()); log.debug("t2 state {}", t2.getState()); log.debug("t3 state {}", t3.getState()); log.debug("t4 state {}", t4.getState()); log.debug("t5 state {}", t5.getState()); log.debug("t6 state {}", t6.getState()); } }
15:55:24.411 c.TestState [t3] - running... 15:55:24.910 c.TestState [main] - t1 state NEW 15:55:24.912 c.TestState [main] - t2 state RUNNABLE 15:55:24.912 c.TestState [main] - t3 state TERMINATED 15:55:24.912 c.TestState [main] - t4 state TIMED_WAITING 15:55:24.912 c.TestState [main] - t5 state WAITING 15:55:24.912 c.TestState [main] - t6 state BLOCKED
2. 线程安全问题
1. 出现线程安全的原因
-
出现线程安全的原因是上下文切换使得各个线程的指令交错执行,比如以下的代码:
@Slf4j(topic = "c.TestThread") public class TestThread { static int count = 0; public static void main(String[] args) throws InterruptedException { final Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { count++; } }); final Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { count--; } }); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("count = {}", count); } } // 输出:17:06:01.717 c.TestThread [main] - count = -1273
可以看到最终的结果并不是我们期望的0;因为在底层对自增以及自减的操作并不是原子的,会分为取,修改,更新。比如当一个线程取出 count = 8,并修改为9,但是还没有来得及将count更新为9就被阻塞了,另一个线程取出的仍然是8,然后减为7,并将count更新为7,此时再调度执行加法的线程,就会把count覆盖为9。但是实际上在两个线程执行完之后原变量的值应该保持不变。
-
如果多个线程共享资源的话,单单进行读操作是没有问题的,但是当多个线程对共享变量进行读写操作的时候就会出现线程安全问题;临界区的概念就是一段代码如果存在对共享资源的多线程续写操作的话,那么就称这个代码区域叫临界区,共享资源也叫作临界资源;
-
竞态条件:多个线程对临界区的访问所造成的线程之间的交错执行就是竞态条件;
-
可以使用synchronized解决以上的问题;该解决方式是阻塞式的解决方案,阻塞式的解决方案还包括使用Lock,除此之外,还可以使用CAS实现的原子变量提供的非阻塞方式来实现线程安全。synchronized使用对象锁来对临界区进行互斥,如果有一个线程已经进入了临界区,其它的线程就不会进入临界区,因为此时的对象锁正在被持有,除非之前的线程释放对对象锁的持有,否则的话之后尝试进入临界区的线程都会被阻塞;必须保证对多个线程的互斥使用的是同一个对象锁,不然的话并不能实现互斥:
@Slf4j(topic = "c.TestThread") public class TestThread { static int count = 0; static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { final Thread t1 = new Thread(() -> { synchronized (lock) { for (int i = 0; i < 5000; i++) { count++; } } }); final Thread t2 = new Thread(() -> { synchronized (lock) { for (int i = 0; i < 5000; i++) { count--; } } }); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("count = {}", count); } }
互斥是指只能有一个线程进入临界区执行,最终得到的效果是串行的;而同步是指一个线程必须等待另一个线程执行结束才能够执行,也就是需要保持一个线程必须在另一个线程执行之前执行完毕,这样后一个线程所需要的条件才能够被满足;
-
synchronized加在成员方法上,使用的锁是this对象;加在static方法(类方法)上使用的所就是类对象:
public class Demo { public synchronized void test() { } //等价于 public void test() { synchronized(this) { } } } public class Demo { public synchronized static void test() { } //等价于 public void test() { synchronized(Demo.class) { } } }
-
对于 synchronized的几种情况,其实考察的就是synchronized是用的哪一个对象作为锁对象;
- 将synchronized加到成员方法上,使用的就是this作为锁对象,所以在以下的代码中两个线程使用的是同一个锁对象,可以实现互斥,最终的结果是先输出1或者2,再输出另一个数字:
@Slf4j(topic = "c.TestThread") public class TestThread { public synchronized void a() { log.debug("1"); } public synchronized void b() { log.debug("2"); } public static void main(String[] args) { final TestThread e1 = new TestThread(); new Thread(() -> e1.a()).start(); new Thread(() -> e1.b()).start(); } }
- a,b锁住的是同一个对象,都是this(e1对象),c没有上锁,结果就是3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1:
输出:@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象就是this, 也就是e1 public synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象也是this, e1 public synchronized void b() { log.debug("2"); } public void c() { log.debug("3"); } public static void main(String[] args) { TestThread e1 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e1.b()).start(); new Thread(() -> e1.c()).start(); } }
21:26:12.555 c.TestThread [Thread-2] - 3 21:26:13.551 c.TestThread [Thread-0] - 1 21:26:13.551 c.TestThread [Thread-1] - 2
- a锁住对象this(e1对象),b锁住对象this(e2对象),不互斥。结果为:2,1s后1
@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象是e1 public synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象是e2 public synchronized void b() { log.debug("2"); } public static void main(String[] args) { TestThread e1 = new TestThread(); TestThread e2 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e2.b()).start(); } } 21:37:53.940 c.TestThread [Thread-1] - 2 21:37:54.937 c.TestThread [Thread-0] - 1
- a锁住的是TestThread.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1:
@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象是TestThread.class类对象 public static synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象是e2 public synchronized void b() { log.debug("2"); } public static void main(String[] args) { final TestThread e1 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e1.b()).start(); } } 21:40:45.312 c.TestThread [Thread-1] - 2 21:40:46.310 c.TestThread [Thread-0] - 1
- a,b锁住的是TestThread.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象是TestThread.class类对象 public static synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象是TestThread.class类对象 public static synchronized void b() { log.debug("2"); } public static void main(String[] args) { TestThread e1 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e1.b()).start(); } } 21:47:30.163 c.TestThread [Thread-0] - 1 21:47:30.166 c.TestThread [Thread-1] - 2
- a锁住的是TestThread.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象是TestThread.class类对象 public static synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象是this,e2对象 public synchronized void b() { log.debug("2"); } public static void main(String[] args) { TestThread e1 = new TestThread(); TestThread e2 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e2.b()).start(); } } 21:44:24.151 c.TestThread [Thread-1] - 2 21:44:25.150 c.TestThread [Thread-0] - 1
- a,b锁住的是TestThread.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
@Slf4j(topic = "c.TestThread") public class TestThread { // 锁对象是TestThread.class类对象 public static synchronized void a() throws InterruptedException { Thread.sleep(1000); log.debug("1"); } // 锁对象是TestThread.class类对象 public static synchronized void b() { log.debug("2"); } public static void main(String[] args) { TestThread e1 = new TestThread(); TestThread e2 = new TestThread(); new Thread(() -> { try { e1.a(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> e2.b()).start(); } } 21:50:14.203 c.TestThread [Thread-0] - 1 21:50:14.207 c.TestThread [Thread-1] - 2
- 将synchronized加到成员方法上,使用的就是this作为锁对象,所以在以下的代码中两个线程使用的是同一个锁对象,可以实现互斥,最终的结果是先输出1或者2,再输出另一个数字:
2. 线程安全分析
-
对于成员变量以及静态变量的线程安全来说,如果变量没有在线程之间共享的话那么变量将会是安全的;如果变量在线程间是共享的但是只有读操作,那么也是线程安全的;如果有读写操作的话,则会在临界区发生竞态条件,需要考虑线程安全;
-
对于局部变量来说,如果局部变量被初始化为基本数据类型的话将会是线程安全的;但是如果局部变量是对象引用的话就可能会出现线程安全问题,如果对象没有逃离方法的作用范围,它就是线程安全的,如果对象逃离了方法的作用范围那么它是线程不安全的;
-
一个成员变量的例子:
@Slf4j(topic = "c.TestThread") public class TestThread { public static void main(String[] args) { UnsafeTest unsafeTest = new UnsafeTest(); for (int i =0;i<100;i++){ new Thread(()->{ unsafeTest.method1(); },"线程"+i).start(); } } } class UnsafeTest{ ArrayList<String> arrayList = new ArrayList<>(); public void method1(){ for (int i = 0; i < 100; i++) { method2(); method3(); } } private void method2() { arrayList.add("1"); } private void method3() { arrayList.remove(0); } } Exception in thread "线程73" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at com.zhyn.UnsafeTest.method3(TestThread.java:33) at com.zhyn.UnsafeTest.method1(TestThread.java:26) at com.zhyn.TestThread.lambda$main$0(TestThread.java:15) at java.lang.Thread.run(Thread.java:748)
线程之间会共享同一个arrayList,如果增加元素的线程还没有完成就被中断,接下来删除元素的线程就会开始工作,删除操作的线程会在列表为空的时候进行删除操作,所以会得到IndexOutOfBoundsException: Index: 0, Size: 0异常报告;
-
可以将列表对象作为局部变量,如下所示:
class UnsafeTest { public void method1() { ArrayList<String> arrayList = new ArrayList<>(); for (int i = 0; i < 100; i++) { method2(arrayList); method3(arrayList); } } private void method2(List<String> arrayList) { arrayList.add("1"); } private void method3(List<String> arrayList) { arrayList.remove(0); } }
列表对象在每个线程调用时都会创建不同的实例,没有进行共享,method2和method3的参数都是由method1传递过来的,所以最终是线程安全的:
但是如果将method2以及method3的访问修饰符修改为public的,有多个线程调度方法,并且有一个子类覆盖了以上的方法,那么将会出现线程安全问题;因为在子类之中又创建了一个新的线程,该线程共享了list,并执行删除操作,对于list的增加以及删除操作就不是在一个线程中进行的了,所以会出现线程安全问题:class ThreadSafe { public final void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList<String> list) { list.add("1"); } public void method3(ArrayList<String> list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ @Override public void method3(ArrayList<String> list) { new Thread(() -> { list.remove(0); }).start(); } }
3. 常见的线程安全的类
- 常见的有String,Integer,StringBuffer,Random,Vector,Hashtable,java.util.concurrent 包下的类;可以理解为以上的类中的每一个方法是线程安全的,但是当多个方法组合在一起使用的时候仍然是线程不安全的:
Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); }
- String是不可变的,当我们对字符串进行修改的时候实际上是又另外创建了一个字符串,字符串本身是不可变的;
4. 线程安全分析示例
- Servlet运行在Tomcat环境下并只有一个实例,因此会被Tomcat的多个线程共享使用,所以存在成员变量的共享问题:
public class MyServlet extends HttpServlet { // 是否安全? 否:HashMap不是线程安全的,HashTable是 Map<String,Object> map = new HashMap<>(); // 是否安全? 是:String 为不可变类,线程安全 String S1 = "..."; // 是否安全? 是 final String S2 = "..."; // 是否安全? 否:不是常见的线程安全类 Date D1 = new Date(); // 是否安全? 否:引用值D2不可变,但是日期里面的其它属性比如年月日可变。与字符串的最大区别是Date里面的属性可变。 final Date D2 = new Date(); public void doGet(HttpServletRequest request,HttpServletResponse response) { // 使用上述变量 } }
- Spring中的Bean都是单例的, 会存在多线程共享问题,可以使用环绕通知使start变为局部变量:
@Aspect @Component public class MyAspect { // 是否安全?不安全, 因为MyAspect是单例的 private long start = 0L; @Before("execution(* *(..))") public void before() { start = System.nanoTime(); } @After("execution(* *(..))") public void after() { long end = System.nanoTime(); System.out.println("cost time:" + (end-start)); } }
- 此例是典型的三层模型调用,MyServlet UserServiceImpl UserDaoImpl类都只有一个实例,UserDaoImpl类中没有成员变量,update方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl类中只有一个线程安全的UserDaoImpl类的实例,那么UserServiceImpl类也是线程安全的,同理 MyServlet也是线程安全的:
public class MyServlet extends HttpServlet { // 是否安全 是:UserService不可变,虽然有一个成员变量, // 但是是私有的, 没有地方修改它 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 是:Dao不可变, 其没有成员变量 private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全 是:没有成员变量,无法修改其状态和属性 public void update() { String sql = "update user set password = ? where username = ?"; // 是否安全 是:不同线程创建的conn各不相同,都在各自的栈内存中 try (Connection conn = DriverManager.getConnection("","","")){ // ... } catch (Exception e) { // ... } } }
- 跟示例二大体相似,UserDaoImpl类中有成员变量,那么多个线程可以对成员变量conn 同时进行操作,故是不安全的:
public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全: 不安全; 当多个线程,共享conn, 一个线程拿到conn,刚创建一个连接赋值给conn, 此时另一个线程进来了, 直接将conn.close //另一个线程恢复了, 拿到conn干事情, 此时conn都被关闭了, 出现了问题 private Connection conn = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }
- 跟示例三大体相似,UserServiceImpl类的update方法中UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao对象,新建的对象是线程独有的,所以是线程安全的:
public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { public void update() { UserDao userDao = new UserDaoImpl(); userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全 private Connection = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }
- 私有变量sdf被暴露出去了, 发生了逃逸;其中foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法,因为foo方法可以被重写,导致线程不安全。 在String类中就考虑到了这一点,String类是final的,子类不能重写它的方法:
public abstract class Test { public void bar() { // 是否安全 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); foo(sdf); } public abstract foo(SimpleDateFormat sdf); public static void main(String[] args) { new Test().bar(); } }
public void foo(SimpleDateFormat sdf) { String dateStr = "1999-10-11 00:00:00"; for (int i = 0; i < 20; i++) { new Thread(() -> { try { sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } }).start(); } }
3. synchronized底层原理
1. Java对象头
- 32位虚拟机的Java对象头之中包含两部分的数据,分别是运行时元数据Mark Word和类型指针Klass Word,他们都是32位的,因此对象头是64位的。运行时元数据中包含的信息有哈希值,GC分代年龄,锁状态标志,偏向线程ID和偏向时间戳;类型指针指向类元数据InstanceKlass,用于确定对象所属的类型。指向的是存放在方法区中的类元信息:
如果是数组对象的话对象头还包括数组的长度信息,也是32位
- 和synchronized原理相关联的就是Mark Word,它的结构如下所示;当最后三位是001的时候表示的是正常状态,相当于没有添加synchronized的情况;当最后三位是101的时候,表示的是偏向锁状态,当最后两位是00的时候表示的是轻量级锁状态;是10的时候表示的是重量级锁状态,11表示的是该对象被垃圾回收器标记了,需要被垃圾回收器清理;
64位JVM:
所以一个对象的结构如下图所示:
2. 基于Monitor的synchronized重量级锁
-
synchronized重量级锁的实现是基于Monitor的,它的意思是监视器或管程(觉得翻译成监视器更通俗,但是不知道国内为什么要翻译为管程)。每一个Java对象都关联一个操作系统的Monitor,如果使用synchronized关键字使用一个对象作为对象锁的话,该对象头中的Markword前30位保存的就是一个Monitor的地址;
-
当一个线程(假设是第一个线程)访问synchronized代码块的时候,首先会检查当前的锁对象是不是已经与Monitor相关联了,如果没有关联的话就会将锁对象的对象头中的Markword的前30位指向操作系统的Monitor,让锁对象中的MarkWord和操作系统的Monitor相关联。如果关联成功的话就将对象锁的对象头的后两位置为10。之后该线程就会成为该Monitor的所有者(Owner)。当另一个线程想要进入synchronized代码块的时候也会检查当前的对象锁是不是已经关联了Monitor,此时他会发现对象锁已经和Monitor进行关联了,然后它会检查该Monitor是否已经有所有者了,此时会发现该Monitor的所有者是之前的线程,所以该线程就只能进入EntryList进行等待;如果仅仅是关联了Monitor而没有设置OWNER,那么尝试加锁的线程就会将OWNER的值设置为该线程本身;如果拥有Monitor的线程执行完临界区的代码之后,Monitor的所有者就会空出来,然后通知EntryList阻塞队列之中的线程,使他们通过竞争选择出一个线程称为新的所有者。成为新的所有者的线程就可以进入临界区运行其中的代码:
处于WAITING状态的线程是指它们已经获取到对象锁了,但是需要等待一定条件的满足才能继续执行,比如说等待一个IO操作;对于没有获取到锁对象的线程来说,它们会处于BLOCKED状态,这两个状态都是针对于Java用户态的,对于OS来说其实都是阻塞态;
基于Monitor的重量锁的实现依赖于操作系统的mutex相关指令,所以在加锁的时候会在用户态以及内核态之间进行切换,是一个非常损耗性能的操作,所以在JDK6之后对synchronized进行了优化,引入了轻量级锁,偏向锁,他们是在JVM的层面之上进行加锁的逻辑,并不会进行频繁的用户态和内核态之间的切换,所以性能也会更高; -
我们可以查看synchronized在字节码的层面是如何实现的,比如对于以下的代码:
public class TestSync { static final Object LOCK = new Object(); static int counter = 0; public static void main(String[] args) { synchronized (LOCK) { counter++; } } }
它对应的字节码如下所示:
0 getstatic #2 <com/zhyn/TestSync.LOCK : Ljava/lang/Object;> // <- lock引用(synchronized开始) 3 dup 4 astore_1 // lock 引用 -> slot 1 5 monitorenter // 将lock对象的MarkWord的前30为设置为指向Monitor的指针 6 getstatic #3 <com/zhyn/TestSync.counter : I> // <- i 9 iconst_1 // 准备常数1 10 iadd // +1操作 11 putstatic #3 <com/zhyn/TestSync.counter : I> // -> i 14 aload_1 // <- lock引用 15 monitorexit // 将lock对象的MarkWord重置,唤醒EntryList 16 goto 24 (+8) 19 astore_2 // e -> slot 2 20 aload_1 // <- lock引用 21 monitorexit // 将lock对象的MarkWord重置,唤醒EntryList 22 aload_2 // <- slot 2 (e) 23 athrow // throw e 24 return
以上的代码对应的异常表:
从字节码中可以看到在进入临界区的时候会使用字节码monitorenter,表示对对象锁进行的设置,在离开临界区的时候使用了字节码monitorexit,并且出现了两处,分别表示在正常情况下的退出和在异常情况下的退出,确保在代码出现异常的时候也可以将锁释放掉。 -
关于Monitor的补充:Monitor被翻译为监视器或者是管程,在JVM层进行实现,底层的代码是C++。在OS层面为了支持对同步的实现,提供了很多的同步原语,其中 semaphore 信号量 和 mutex 互斥量是最重要的同步原语,在使用基本的 mutex 进行并发控制时,需要程序员非常小心地控制 mutex 的 down 和 up 操作,否则很容易引起死锁等问题。为了更容易地编写出正确的并发程序,所以在 mutex 和 semaphore 的基础上,提出了更高层次的同步原语 monitor,不过需要注意的是,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持monitor,Java 语言支持 monitor;而Monitor最终是基于操作系统层面的mutex 和 semaphore,开销是比较大的,所以也叫做重量级锁;除此之外,Synchornized并不等同于Monitor,而是Monitor机制包括Synchornized以及一系列线程调用的API比如wait(),notify()方法。开发者要结合使用 synchronized 关键字,以及 Object 的 wait / notify 等元素,才能说自己利用 monitor 的机制去解决了一个生产者消费者的问题。
3. synchronized的优化过程
1. 优化的原因
- 由于synchronized的重量级锁需要频繁的进行用户态和核心态之间进行频繁的切换,所以会带来很大的性能损耗,于是JDK6就对原先的synchronized进行了优化,使得在没有必要使用重量级锁的时候可以使用偏向锁和轻量级锁,它们都是在JVM的层面实现加锁的逻辑,不依赖于底层的OS,所以没有切换所带来的消耗;JDK6对synchronized的优先状态:偏向锁 –> 轻量级锁 –> 重量级锁;
2. 轻量级锁
-
轻量级锁的应用场景是当一个对象被多个线程访问的时候但是访问的时间在程序运行的一段时期内可能是错开的,这时候synchronized使用的就不是重量级锁,而是基于锁记录的轻量级锁;轻量级锁的使用对程序员来说是透明的,我们使用的仍然是synchronized关键字,轻量级锁只是在synchronized满足一定的条件的时候自动做出的优化;轻量级锁的执行步骤如下所示:
-
首先在线程的虚拟机栈栈帧中创建锁记录对象:
-
让锁记录中的Object Reference指向锁对象,并尝试使用CAS的方式将锁对象中的MarkWord与锁记录中的lock record地址项进行交换:
-
CAS成功就表示在对象的对象头之中存储的就是锁记录地址,最后两位是00,;
-
-
如果CAS的操作失败了,那么就代表需要进行锁膨胀的过程或者是需要进行锁重入过程;如果在进行CAS操作的时候对象头之中已经存储了其它线程的锁记录地址项,那么就表示此时出现了竞争,会进行锁膨胀;如果对象头之中存储的是同一个线程的锁记录地址项,那么就会进行锁重入;也就是对同一个锁对象进行重复加锁的情况;
-
当发生锁重入的时候,会再创建一个锁记录对象,多个锁记录对象可以作为加锁的计数器,当轻量级锁解锁的时候就可以根据锁记录的数量进行解锁;对于新创建出的锁记录仍然会指向对应的锁对象:
-
如果CAS的失败是由于竞争而不是锁重入引起的,那么将会发生锁膨胀,锁膨胀的过程如下所示:因为发生的竞争所以就会使用重量级锁,会将对象头中的Mark Word改变为记录Monitor的地址,并将最后两位置为10表示使用的是重量级锁;之后将竞争所得县城管放入到Entry List中:
-
轻量级锁的解锁流程:当线程退出synchronized代码块的时候如果获取的是取值为 null 的锁记录,就说明有锁重入发生,这个时候就将相应的锁记录删除即可;如果所得到的锁记录的值并不是null,那么就表示此时并没有发生锁重入,采取CAS操作将Mark Word的值交还给锁对象就可以了;如果解锁失败的话就表示轻量级锁已经膨胀为重量级锁了,之后将会进行重量级锁的解锁过程;
-
对重量级锁的竞争可以进行自旋优化;当一个线程正在占有锁的时候,如果有另外一个线程想要获得锁,那么它并不会进入到BLOCKED状态,而是进行一定次数的自旋(也就是执行while循环),如果在自旋的过程中持有锁的线程释放了对锁的持有,那么当前线程就不用进入阻塞状态而直接获得锁,也就代表了可以避免发生上下文切换:
如果自旋一定次数之后还是没有得到锁,那么就直接进入BLOCKED状态:
自旋是占用CPU时间的,在单核CPU之上进行自旋就是在浪费CPU,在多核CPU中自旋才会发挥优势;在JDK6之后的自旋是自适应的,比如当一个线程之前针对锁对象自旋成功了,那么在本次的自旋之中就会认为自旋成功的可能性是比较高的,所以就会多自旋几次;反之就会减少自旋的次数甚至是不自旋;在JDK7之后我们不能使用参数来控制是否开启自旋;
3. 偏向锁
-
偏向锁是针对轻量级锁的重入进行的优化;如果一个线程中多次使用synchronized进行锁重入操作,那么再继续使用轻量级锁就是一种浪费,就会使用偏向锁进行优化;这个时候就不会发生尝试使用CAS操作,却因为需要进行锁重入操作而发生失败,自然也不会再创建锁记录;但是在竞争比较多的情况下是不会使用偏向锁的;偏向锁只有在第一次使用CAS的时候将线程ID设置到对象头的MarkWord中,之后发现这个线程ID就是自己,就表示没有竞争,不用再采取CAS操作,以后只要不发生竞争这个对象就归该线程所有;
-
如果开启了偏向锁那么对象创建后Markword的后三位是101,此时它的thread,epoch,age都是0;偏向锁默认是延迟的,不会在程序启动的时候立即生效,如果想要避免延迟的话可以使用-XX:BiasedLockingStartupDelay=0来禁用延迟;如果没有开启对象锁的话,那么在对象创建后Markword的后三位是001,hashcode和age都是0,hashcode在第一次被使用的时候才会被计算出来;hashCode()方法会禁用这个对象的偏向锁;因为hashcode需要占用31位(在64位的JVM中),所以为了存储hashcode就必修撤销偏向锁;轻量级锁的hashcode存储在栈帧中的锁记录中;重量级锁的hashcode存在在Monitor对象之中,解锁的时候还会还原回来,但是偏向锁没有额外的空间存储hashcode;
-
当在偏向锁上出现竞争的时候,会将偏向锁升级为轻量级锁,使用像wait/notify一类的需要重量级锁的机制的时候,也无法使用偏向锁;
-
批量重偏向:如果对象被多个线程访问但是却没有竞争,这时偏向了T1的对象仍然有机会重新偏向至T2,重新偏向会重新设置对象头中的ThreadID,当撤销偏向锁的次数超过阈值20次之后,JVM就会觉得偏向是错误的,于是就会在给这些对象加锁时重新偏向至进行加锁线程;当撤销偏向锁的操作超过阈值40次后,JVM就会发觉自己确实是偏向错了,根本不应该进行偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的;
-
锁消除:线程同步的代价是相当高的,同步的后果是降低并发性和性能;在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程;如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步;这样就能大大提高并发性和性能;这个取消同步的过程就叫同步省略,也叫锁消除。
4. wait和notify
1. wait和notify介绍
- 当一个线程获取到锁称为Monitor的拥有者之后,如果发现自己想要执行synchronized代码块所需的条件还没有满足的话;就可以通过锁对象调用wait方法进入Monitor的WaitSet集合,此时该线程的状态将会变为WAITING,处于BLOCKED以及WAITING的线程在OS都是称为阻塞态,但是二者在Java用户态中所表示的意义是不同的,WAITING是指线程已经获取到了锁对象但是需要等待某种条件的满足才能够继续执行,但是处于BLOCKED的线程是指它根本还没有获取到锁对象;此时的锁对象正在被其他的线程使用,处于BLOCKED状态的线程会在对象锁被释放的时候被唤醒去竞争锁;如果没有竞争到锁的话会继续处于该状态;处于WAITING状态的线程只有被锁对象调用了notify方法或者是notifyAll方法的时候才会被唤醒,然后进入EntryList之中重新竞争锁;
2. 相关API的使用
下面的方法都是Object中的方法,通过锁对象来调用,只有在synchronized代码块内部才能够使用锁对象调用以下的方法:
- wait(): 让获得对象锁的线程到waitSet中一直等待;
- wait(long n) : 该线程在等待指定的时间之后还是没有被notify方法或者是notifyAll方法唤醒的话就会自动唤醒;
- notify():该方法会随机的唤醒一个因为锁对象调用了wait()方法而处于WAITING状态的线程;
- notifyAll():该方法会唤醒所有的因为锁对象调用了wait()方法而处于WAITING状态的线程;
代码示例:
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.WaitNotifyTest")
public class WaitNotifyTest {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (LOCK) {
log.debug("执行...");
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...");
}
}, "t1").start();
new Thread(() -> {
synchronized (LOCK) {
log.debug("执行...");
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...");
}
}, "t2").start();
// 让主线程先睡眠一段时间,不然的话可能以上的线程还没来得及进入WaitSet就已经被唤醒了
Thread.sleep(1000);
log.debug("唤醒WaitSet中的线程");
synchronized (LOCK) {
LOCK.notifyAll();
}
}
}
运行结果:
20:23:58.985 c.WaitNotifyTest [t1] - 执行...
20:23:58.990 c.WaitNotifyTest [t2] - 执行...
20:23:59.984 c.WaitNotifyTest [main] - 唤醒WaitSet中的线程
20:23:59.984 c.WaitNotifyTest [t2] - 其它代码...
20:23:59.984 c.WaitNotifyTest [t1] - 其它代码...
3. sleep(long n) 和 wait(long n)的区别
- 前者是Thread类中的静态方法,后者是Object中的方法;
- 前者在阻塞的时候不会释放锁但是后者在阻塞的时候会释放锁,因为不是释放锁的话其它的线程就不能唤醒该阻塞的线程;
- 前者不需要和synchronized配合使用,但是后者需要和synchronized配合使用,因为后者只能使用对象锁进行调用;
- 不过二者的阻塞状态都是TIMED_WAITING;
4. wait/notify的正确使用
-
以下的问题更适合使用wait/notify而不是sleep解决:
@Slf4j(topic = "c.WaitNotifyTest") public class WaitNotifyTest { static final Object ROOM = new Object(); static boolean hasCigarette = false; static boolean hasTakeOut = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (ROOM) { log.debug("有烟吗?[{}]", hasCigarette); if (!hasCigarette) { log.debug("没有烟..."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟吗?[{]}", hasCigarette); if (hasCigarette) log.debug("有烟了..."); } },"小南").start(); for (int i = 0; i < 5; i++) { new Thread(() -> { synchronized (ROOM) { log.debug("..."); } },"其它人").start(); } Thread.sleep(1000); new Thread(() -> { hasCigarette = true; log.debug("烟到了!"); },"送烟的").start(); } }
输出结果:
08:29:40.166 c.WaitNotifyTest [小南] - 有烟吗?[false] 08:29:40.171 c.WaitNotifyTest [小南] - 没有烟... 08:29:41.166 c.WaitNotifyTest [送烟的] - 烟到了! 08:29:42.172 c.WaitNotifyTest [小南] - 有烟吗?[true] 08:29:42.172 c.WaitNotifyTest [小南] - 有烟了... 08:29:42.172 c.WaitNotifyTest [其它人] - ... 08:29:42.172 c.WaitNotifyTest [其它人] - ... 08:29:42.172 c.WaitNotifyTest [其它人] - ... 08:29:42.172 c.WaitNotifyTest [其它人] - ... 08:29:42.172 c.WaitNotifyTest [其它人] - ...
如果给送烟的线程加上synchronized的话,那么小南线程最终将不会被启动:
08:25:22.556 c.WaitNotifyTest [小南] - 有烟吗?[false] 08:25:22.560 c.WaitNotifyTest [小南] - 没有烟... 08:25:24.562 c.WaitNotifyTest [小南] - 有烟吗?[false] 08:25:24.564 c.WaitNotifyTest [其它人] - ... 08:25:24.564 c.WaitNotifyTest [送烟的] - 烟到了! 08:25:24.564 c.WaitNotifyTest [其它人] - ... 08:25:24.565 c.WaitNotifyTest [其它人] - ... 08:25:24.565 c.WaitNotifyTest [其它人] - ... 08:25:24.566 c.WaitNotifyTest [其它人] - ...
可以看到其它的线程必须在小南线程睡眠的时候一直处于阻塞的状态,只有在小南线程醒来的时候才可以继续向下运行;并且小南线程必须睡眠指定的时间,即使送烟的线程提前将烟送来也不行,除此之外,如果对送烟的线程也加锁的话小南最终是无法干活的,解决方法就是使用wait-notify机制解决该问题;
-
使用wait/notify解决以上问题:
@Slf4j(topic = "c.WaitNotifyTest") public class WaitNotifyTest { static final Object ROOM = new Object(); static boolean hasCigarette = false; static boolean hasTakeOut = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (ROOM) { log.debug("有烟吗?[{}]", hasCigarette); if (!hasCigarette) { log.debug("没有烟..."); try { ROOM.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟吗?[{}]", hasCigarette); if (hasCigarette) log.debug("有烟了..."); } },"小南").start(); for (int i = 0; i < 5; i++) { new Thread(() -> { synchronized (ROOM) { log.debug("..."); } },"其它人").start(); } Thread.sleep(1000); new Thread(() -> { synchronized (ROOM) { hasCigarette = true; log.debug("烟到了!"); ROOM.notify(); } },"送烟的").start(); } }
输出结果:
08:51:29.538 c.WaitNotifyTest [小南] - 有烟吗?[false] 08:51:29.543 c.WaitNotifyTest [小南] - 没有烟... 08:51:29.543 c.WaitNotifyTest [其它人] - ... 08:51:29.543 c.WaitNotifyTest [其它人] - ... 08:51:29.543 c.WaitNotifyTest [其它人] - ... 08:51:29.543 c.WaitNotifyTest [其它人] - ... 08:51:29.543 c.WaitNotifyTest [其它人] - ... 08:51:30.536 c.WaitNotifyTest [送烟的] - 烟到了! 08:51:30.536 c.WaitNotifyTest [小南] - 有烟吗?[true] 08:51:30.537 c.WaitNotifyTest [小南] - 有烟了...
如果还有另外的线程也在等待某种条件,notify方法就会随机的唤醒其中的一个线程:
@Slf4j(topic = "c.WaitNotifyTest") public class WaitNotifyTest { static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]", hasCigarette); if (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); // 此时进入到waitset等待集合, 同时会释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } } }, "小南").start(); new Thread(() -> { synchronized (room) { log.debug("外卖送到没?[{}]", hasTakeout); if (!hasTakeout) { log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小女").start(); Thread.sleep(1000); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖到了噢!"); room.notify(); } }, "送外卖的").start(); } }
输出结果:
09:09:32.746 c.WaitNotifyTest [小南] - 有烟没?[false] 09:09:32.754 c.WaitNotifyTest [小南] - 没烟,先歇会! 09:09:32.754 c.WaitNotifyTest [小女] - 外卖送到没?[false] 09:09:32.754 c.WaitNotifyTest [小女] - 没外卖,先歇会! 09:09:33.740 c.WaitNotifyTest [送外卖的] - 外卖到了噢! 09:09:33.741 c.WaitNotifyTest [小南] - 有烟没?[false]
可以看到外卖送到了但是却唤醒了小南线程,可以使用notifyAll方法;但是这样做小南线程还是会被唤醒并检查烟有没有被送到,这就是虚假唤醒,即在线程等待的条件还没有被满足的时候线程就已经被唤醒了;可以使用while循环的方式来解决虚假唤醒的问题:
@Slf4j(topic = "c.WaitNotifyTest") public class WaitNotifyTest { static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]", hasCigarette); while (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); // 此时进入到waitset等待集合, 同时会释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } } }, "小南").start(); new Thread(() -> { synchronized (room) { log.debug("外卖送到没?[{}]", hasTakeout); while (!hasTakeout) { log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小女").start(); Thread.sleep(1000); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖到了噢!"); room.notifyAll(); } }, "送外卖的").start(); } }
输出:
09:19:11.147 c.WaitNotifyTest [小南] - 有烟没?[false] 09:19:11.151 c.WaitNotifyTest [小南] - 没烟,先歇会! 09:19:11.151 c.WaitNotifyTest [小女] - 外卖送到没?[false] 09:19:11.151 c.WaitNotifyTest [小女] - 没外卖,先歇会! 09:19:12.145 c.WaitNotifyTest [送外卖的] - 外卖到了噢! 09:19:12.145 c.WaitNotifyTest [小女] - 外卖送到没?[true] 09:19:12.145 c.WaitNotifyTest [小女] - 可以开始干活了 09:19:12.145 c.WaitNotifyTest [小南] - 没烟,先歇会!
使用while循环即使线程被唤醒了但是并不会跑到循环之外执行询问的代码,当被唤醒的时候只需要检查唤醒的条件是不是自己所需的条件;
5. 同步模式之保护性暂停
-
该模式是指有一个结果需要从一个线程传递到另一个线程;如果结果不断地从一个线程传递到另一个线程可以使用消息队列解决,典型的生产者消费者问题使用的就是消息队列;JDK的join以及Future的原理就是该模式;
-
为了实现解耦合,可以在两个线程之间添加一个保护类来存储结果:
-
以下展示了该模式的一种使用情况:
@Slf4j(topic = "c.GuardedObjectTest") public class GuardedObjectTest { public static void main(String[] args) { // 线程1等待线程2的下载结果 GuardedObject guardedObject = new GuardedObject(); new Thread(() -> { log.debug("等待结果"); List<String> list = (List<String>) guardedObject.get(); log.debug("结果大小:{}", list.size()); }, "t1").start(); new Thread(() -> { List<String> list = new ArrayList<>(); list.add("Guarded"); list.add("Object"); guardedObject.set(list); }, "t2").start(); } } class GuardedObject { // 结果 private Object response; // 获取结果 public Object get() { synchronized (this) { // 防止虚假唤醒 // 没有结果 while (response == null) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return response; } } // 产生结果 public void set(Object response) { synchronized (this) { // 给结果变量赋值 this.response = response; this.notifyAll(); } } }
输出:
09:54:08.157 c.GuardedObjectTest [t1] - 等待结果 09:54:08.160 c.GuardedObjectTest [t1] - 结果大小:2
-
可以设置超时时间,在限定的时间范围内还是没有结果的话就直接退出while循环:
@Slf4j(topic = "c.GuardedObjectTest") public class GuardedObjectTest { public static void main(String[] args) { // 线程1等待线程2的下载结果 GuardedObject guardedObject = new GuardedObject(); new Thread(() -> { log.debug("等待结果"); Object object = guardedObject.get(2000); log.debug("结果:{}", object); }, "t1").start(); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } guardedObject.set(new Object()); }, "t2").start(); } } class GuardedObject { // 结果 private Object response; public Object get(long timeOut) { synchronized (this) { long begin = System.currentTimeMillis(); long passTime = 0L; while (response == null) { long waitTime = timeOut - passTime; if (waitTime <= 0) break; try { this.wait(waitTime); } catch (InterruptedException e) { e.printStackTrace(); } passTime = System.currentTimeMillis() - begin; } return response; } } // 产生结果 public void set(Object response) { synchronized (this) { // 给结果变量赋值 this.response = response; this.notifyAll(); } } }
输出:
10:10:33.887 c.GuardedObjectTest [t1] - 等待结果 10:10:34.882 c.GuardedObjectTest [t1] - 结果:java.lang.Object@27608642
超时输出:
10:12:21.862 c.GuardedObjectTest [t1] - 等待结果 10:12:23.867 c.GuardedObjectTest [t1] - 结果:null
关于等待时间的使用,在有参数的join方法(其实无参的join函数调用的就是参数为0的join方法)中可以看到同样的使用方式:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
在实际的应用之中等待结果的线程和产生结果的线程往往是多个的,为了解耦两者之间的关联,同样也可以使用一个中间类;等待结果的线程必须进行同步,所以该模式划分到同步模式;
6. 异步模式之生产者消费者问题
- 该问题与之前的保护性暂停不同的是不需要产生结果和消费结果是一一对应的;
- 生产者只需要产生数据,不关心数据是怎么被处理的,消费者也只需要关心数据的处理;
- 消息队列的容量是有限的,当队列为空的时候消费者会阻塞等待,当队列满的时候生产者线程需要阻塞等待;
- 异步模式中生产者生产出来的数据不会被立刻消费,但是在同步模式之中产生的数据会立刻被消费;
@Slf4j(topic = "c.ProductConsumerTest")
public class ProductConsumerTest {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id, "值" + id));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message message = queue.take();
}
}, "消费者").start();
}
}
// 消息队列类,在线程之间通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的队列集合
private final LinkedList<Message> list = new LinkedList<>();
// 队列容量
private final int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
// 获取消息
public Message take() {
// 检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
log.debug("队列为空, 消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列头部获取消息并返回
Message message = list.removeFirst();
log.debug("已消费消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 检查对象是否已满
while (list.size() == capcity) {
try {
log.debug("队列已满, 生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将消息加入队列尾部
list.addLast(message);
log.debug("已生产消息 {}", message);
list.notifyAll();
}
}
}
final class Message {
private final int id;
private final Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
输出:
10:42:18.563 c.MessageQueue [生产者1] - 已生产消息 Message{id=1, value=值1}
10:42:18.576 c.MessageQueue [生产者0] - 已生产消息 Message{id=0, value=值0}
10:42:18.577 c.MessageQueue [生产者2] - 队列已满, 生产者线程等待
10:42:19.552 c.MessageQueue [消费者] - 已消费消息 Message{id=1, value=值1}
10:42:19.552 c.MessageQueue [生产者2] - 已生产消息 Message{id=2, value=值2}
10:42:20.553 c.MessageQueue [消费者] - 已消费消息 Message{id=0, value=值0}
10:42:21.554 c.MessageQueue [消费者] - 已消费消息 Message{id=2, value=值2}
10:42:22.554 c.MessageQueue [消费者] - 队列为空, 消费者线程等待
5. park & unpark
1. 基本使用
- 以上的两个方法都是LockSupport类中的方法,park方法是是线程处于等待的状态直到调用了unpark方法来唤醒该线程:
如果我们在调用park方法之前调用了unpark方法,那么当调用park方法的时候并不会等待而是直接继续运行;// 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(thread);
2. 方法的原理
-
每一个线程都有一个Parker对象,但是该对象是在C/C++层面的,我们在Java层面无法查看,该对象有三部分组成:_counter,_cond和 _mutex;
-
可以把线程比作一个正在旅行的旅人,Parker就像是它随身携带的背包,条件变量就好比是背包中的帐篷。_counter就好比是背包中的干粮;调用park方法的时候就相当于查看是不是需要休息,如果背包中还有干粮的话(0为耗尽,1为充足)就不需要休息,也就是不需要线程进入等待状态;否则的换线程就需要进入等待状态;调用unpark方法的时候就相当于是补充干粮的数量(其实只是将他的值置为1);如果此时线程在等待的话就唤醒线程,如果此时没有因为调用了park方法而处于等待状态的线程的话,那么就什么都不用做,当下一次有线程调用park方法的时候仅需要将干粮的值置为0,不需要等待;多次调用unpark方法的话干粮的值也只会是1;
(1) 当前线程调用Unsafe.park()方法;
(2) 检查_counter,此时是0,需要获得_mutex互斥锁;
(3) 线程进入_cond条件变量阻塞;
(4) 设置_counter = 0;
(1) 调用Unsafe.unpark方法,设置_counter = 1;
(2) 唤醒_cond条件变量中的线程;
(3) 线程恢复运行;
(4) 设置_counter = 0;
(1) 先调用了Unsafe.unpark方法,设置_counter = 1;
(2) 之后当前线程才调用了park方法;
(3) 检查_counter,此时为1,线程不需要阻塞,直接继续运行;
(4) 设置_counter = 0;
6. 线程间的状态转换
-
NEW <–> RUNNABLE
:调用start()方法时, NEW --> RUNNABLE;对应 1 处; -
RUNNABLE <–> WAITING
:- 线程用synchronized(obj)获取了对象锁后,调用 obj.wait()方法时,t 线程进入waitSet中, 发生转换RUNNABLE --> WAITING,调用 obj.notify(),obj.notifyAll(),t.interrupt() 时, 唤醒的线程都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列和其他线程再进行竞争锁;竞争成功了就转换为RUNNABLE状态,不成功的话装换为BLOCKED状态;对应 2 处;
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING ,注意是当前线程在waitSet上等待;t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE;对应 3 处;
- 当前线程调用 LockSupport.park() 方法会让当前线程从RUNNABLE --> WAITING;调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE;对应 4 处;
-
RUNNABLE <–> TIMED_WAITING
:- t 线程用synchronized(obj) 获取了对象锁后,调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING;t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时; 线程被唤醒,然后都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列和其他线程再进行竞争锁;竞争成功了就转换为RUNNABLE状态,不成功的话装换为BLOCKED状态;对应 5 处;
- 当前线程调用 t.join(long n) 方法时,会发生状态转换 RUNNABLE --> TIMED_WAITING ,当前线程在WaitSet中等待;当前线程等待时间超过了 n 毫秒,或t 线程运行结束,亦或是调用了当前线程的 interrupt() 方法时,当前线程发生状态转换 TIMED_WAITING --> RUNNABLE;对应 6 处。
- 当前线程调用 Thread.sleep(long n)方法时 ,当前线程发生状态转换 RUNNABLE --> TIMED_WAITING;当线程等待的时间超过了指定的时间之后,或调用了线程的 interrupt() 方法,当前线程发生状态转换 TIMED_WAITING --> RUNNABLE,对应 7 处。
- 当前线程调用 Thread.sleep(long n)方法时 ,会发生状态转换 RUNNABLE --> TIMED_WAITING;调用LockSupport.unpark(目标线程) 或调用了 interrupt() 方法的时候,亦或是等待超时,就会发生状态转换 TIMED_WAITING–> RUNNABLE;对应 8 处;
-
RUNNABLE <–> BLOCKED
:如果 t 线程用 synchronized(obj) 尝试获取对象锁时竞争失败了,就会从 RUNNABLE –> BLOCKED;当持有对象锁的线程执行完代码块中的代码时,就会唤醒该对象上所有处于 BLOCKED状态 的线程重新竞争对象锁,如果其中的 t 线程竞争成功了,就会从 BLOCKED –> RUNNABLE ,其它竞争失败的线程仍然处于 BLOCKED状态;对应 9 处; -
RUNNABLE <–> TERMINATED
:当线程运行完所有所需的代码的时候,就会进入 TERMINATED,对应 10 处;
7. 多把锁
- 对于以下的场景来说只使用一把锁是不够的:一间大房子里面有两个功能,分别是睡觉和学习;二者互不相干,现在小南想要睡觉但是小北却想要学习,如果只是一个对象锁的话,那么并发度将会是很低的,比如在小南获取到锁进行睡觉的时候小北只能够等待小南醒来;解决方法就是准备多把锁,睡觉的线程使用一把锁,学习的线程使用一把锁:
输出:@Slf4j(topic = "c.BigRoomTest") public class BigRoomTest { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> { try { bigRoom.sleep(); } catch (InterruptedException e) { e.printStackTrace(); } }, "小南").start(); new Thread(() -> { try { bigRoom.study(); } catch (InterruptedException e) { e.printStackTrace(); } }, "小北").start(); } } @Slf4j(topic = "c.BigRoom") class BigRoom { public void sleep() throws InterruptedException { synchronized (this) { log.debug("sleeping 2 小时"); Thread.sleep(2000); } } public void study() throws InterruptedException { synchronized (this) { log.debug("study 1 小时"); Thread.sleep(1000); } } }
可看到二者其实是串行执行的,并发的程度并不高;为了解决以上的问题,可以使用两把锁提高并行性:15:34:34.486 c.BigRoom [小南] - sleeping 2 小时 15:34:36.490 c.BigRoom [小北] - study 1 小时
输出:private static final BigRoom LOCK1 = new BigRoom(); private static final BigRoom LOCK2 = new BigRoom();
以上的做法其实是降低了锁的粒度,降低锁的粒度可以增加并发度,但是却更容易发生死锁;15:42:12.763 c.BigRoom [小北] - study 1 小时 15:42:12.763 c.BigRoom [小南] - sleeping 2 小时
8. 活跃性
1. 活跃性介绍
- 因为某种原因使得代码一直不能执行完毕,这种现象叫做活跃性;
- 活跃性相关的一系列的问题都可以通过可重入锁ReentrantLock解决;
2. 死锁
- 当一个线程需要获取多把锁的时候,就很容易发生死锁,并且死锁的检测是很困难的,比如一个线程获取了对象锁A,同时又想获取对象锁B;另一个线程获取了对象锁B,同时又想获取对象锁A;它们都持有着对方想要获取的锁,但是又同时期望着对方能够释放掉自己持有的锁,这样的话两个线程都不能够继续运行,这就是死锁现象;
- 发生死锁的四种必要条件分别是互斥条件,请求和保持条件,不可抢占条件和循环等待条件;互斥条件是指在一段时间内一个资源只能够被一个线程访问;请求和保持条件是指线程已经拥有了至少一个条件,同时又去申请其它的资源;因为其它的资源被别的线程所持有,所以该线程会进入阻塞状态,并且不释放自己已经持有的资源;不可抢占是指线程对已经获得的资源在未使用完成之前是不能够被别的线程抢占的,只能在自己使用完之后释放掉;循环等待条件是指发生死锁时不定存在一个线程的资源循环链。在解决死锁问题的时候只需要破坏以上的任意一个条件就可以了。
- 以下展示了使用 jps + jstack 的方式来检测死锁:
以下展示了使用 jconsole 的方式来检测死锁
- 一个典型的死锁问题就是哲学家就餐问题,它们只会做两件事,一件事是思考,另一个是吃饭,吃饭的时候需要同时持有两只筷子,但是一共有5个科学家却只有5只筷子,如果筷子被别人拿着,那么自己就得等待;如果每一个哲学家都拿走自己左手边的筷子,那么最后肯定会发生死锁;可以在线程使用锁的时候采取固定的加锁顺序来避免死锁,比如可以使用Hash值的大小来确定加锁的顺序;也可以尽可能的缩小加锁的范围,等到操作共享变量的时候再加锁;使用可释放的定时锁,该锁会在一段时间之后自动释放,所以可以解决死锁的问题;
- 还有一种情况叫做活锁,也就是线程一直在互相改变对方的相关数据,使他们一直不能够达到结束条件,线程就会一直运行,谁都不会结束,比如以下的代码:
输出:@Slf4j(topic = "c.Test") public class Test { static volatile int count = 10; static final Object LOCK = new Object(); public static void main(String[] args) { new Thread(() -> { while (count > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { while (count < 20) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count++; log.debug("count: {}", count); } }, "t2").start(); } }
可以看到以上的两个线程在一直运行,两个线程的结束条件都达不到;16:53:59.572 c.Test [t1] - count: 11 16:53:59.572 c.Test [t2] - count: 11 16:53:59.778 c.Test [t1] - count: 10 16:53:59.778 c.Test [t2] - count: 11 16:53:59.978 c.Test [t2] - count: 11 16:53:59.978 c.Test [t1] - count: 10 16:54:00.178 c.Test [t1] - count: 10 16:54:00.178 c.Test [t2] - count: 10 16:54:00.380 c.Test [t1] - count: 10 16:54:00.380 c.Test [t2] - count: 11 16:54:00.580 c.Test [t1] - count: 9 16:54:00.580 c.Test [t2] - count: 10 16:54:00.780 c.Test [t2] - count: 11 16:54:00.780 c.Test [t1] - count: 11 16:54:00.982 c.Test [t2] - count: 12 16:54:00.982 c.Test [t1] - count: 12 16:54:01.182 c.Test [t1] - count: 13 16:54:01.182 c.Test [t2] - count: 13 16:54:01.382 c.Test [t2] - count: 13 16:54:01.382 c.Test [t1] - count: 12 16:54:01.585 c.Test [t2] - count: 14 16:54:01.585 c.Test [t1] - count: 13 16:54:01.786 c.Test [t1] - count: 12 16:54:01.786 c.Test [t2] - count: 13 16:54:01.987 c.Test [t2] - count: 14 16:54:01.987 c.Test [t1] - count: 13 16:54:02.188 c.Test [t2] - count: 13 16:54:02.188 c.Test [t1] - count: 12 16:54:02.388 c.Test [t1] - count: 12 16:54:02.388 c.Test [t2] - count: 12 16:54:02.590 c.Test [t1] - count: 11 16:54:02.590 c.Test [t2] - count: 12 16:54:02.790 c.Test [t2] - count: 11 16:54:02.790 c.Test [t1] - count: 11 16:54:02.990 c.Test [t2] - count: 12 16:54:02.990 c.Test [t1] - count: 12 16:54:03.192 c.Test [t2] - count: 12 16:54:03.192 c.Test [t1] - count: 12 16:54:03.394 c.Test [t2] - count: 12 16:54:03.394 c.Test [t1] - count: 11 16:54:03.594 c.Test [t1] - count: 11 16:54:03.594 c.Test [t2] - count: 12 16:54:03.795 c.Test [t1] - count: 13 16:54:03.795 c.Test [t2] - count: 13 16:54:03.996 c.Test [t1] - count: 12 16:54:03.996 c.Test [t2] - count: 13
- 饥饿:有一些线程会因为优先级太低而一直抢不到对象锁;导致该线程一直处于BLOCKED状态得不到运行;
9. ReentrantLock
1. ReentrantLock使用
-
ReentrantLock的特点:
- 支持锁重入,就是同一个线程在获取到锁之后还会反复的获取锁,这个时候就不需要对锁进行竞争,因为线程本来就是所得拥有者,所以只用进行简单的记录就可以了;
- 可中断:ReentrantLock是可以被中断的:
lock.lockInterruptibly()
- 可以设置超时时间:如果一个线程在尝试获取锁对象时所用的时间超过了指定的时间,那么就不会再继续尝试获取锁:
lock.tryLock(时间)
- 可以设置为公平锁:ReentrantLock默认是非公平的,可以使用相应的方法并传入参数使ReentrantLock变为公平锁,公平锁是指线程对锁的使用是先到先得的;
- 支持多个条件变量,也即是有多个waitset,通过不同的条件变量可以使线程在不同的条件下进行等待,从而在需要的时候唤醒特定的线程;
- 基本语法:
//获取ReentrantLock对象 private ReentrantLock lock = new ReentrantLock(); //加锁 lock.lock(); try { //需要执行的代码 } finally { //释放锁 lock.unlock(); }
-
支持锁重入的演示:在前面提到过,可重入锁是指已经拥有了锁的线程有权利再次获取到这把锁;如果不是可重入锁的话,那么在第二次获取所得时候,已经拥有锁的线程自己也会被锁挡住:
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static ReentrantLock LOCK = new ReentrantLock(); public static void main(String[] args) { LOCK.tryLock(); log.debug("entry main"); m1(); LOCK.unlock(); } private static void m1() { LOCK.lock(); log.debug("entry m1"); m2(); LOCK.unlock(); } private static void m2() { log.debug("entry m2"); } }
输出:
19:14:20.626 c.ReentrantTest [main] - entry main 19:14:20.631 c.ReentrantTest [main] - entry m1 19:14:20.632 c.ReentrantTest [main] - entry m2
-
可打断是指在一个线程等待锁的过程中,其它的线程可以打断等待的线程,默认是不可打断的,也就是线程在获取不到锁的时候必须是一直等待的;可打断锁可以终止线程的等待;被打断的线程就会结束自己的运行;该方法可以在一定程度之上减少死锁的概率,因为线程不会一直等下去:
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static ReentrantLock LOCK = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { LOCK.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("在等待锁的过程中被打断"); return; } }, "t1"); LOCK.lock(); log.debug("主线程获取到了锁"); t1.start(); Thread.sleep(1000); t1.interrupt(); log.debug("执行打断"); LOCK.unlock(); } }
输出:
19:44:35.026 c.ReentrantTest [main] - 主线程获取到了锁 19:44:36.033 c.ReentrantTest [main] - 执行打断 19:44:36.034 c.ReentrantTest [t1] - 在等待锁的过程中被打断 java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at com.zhyn.ReentrantTest.lambda$main$0(ReentrantTest.java:14) at java.lang.Thread.run(Thread.java:748)
使用lock()方法的话等待锁的线程是不会被打断的,它会一直等待直到获取到锁;
-
锁超时:尝试获得锁的线程在等待了指定的时间之后如果还是没有获取到锁的话就会放弃本次对锁的获取;从而防止无限制的等待;对应的方法不设置等待时间的话尝试获取锁失败的时候会立即放弃对锁的获取并抛出异常:
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static ReentrantLock LOCK = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { log.debug("尝试获得锁"); try { if (!LOCK.tryLock(1, TimeUnit.SECONDS)) { log.debug("尝试获得锁超时,直接返回"); return; } } catch (InterruptedException e) { log.debug("被打断了,获取锁失败,抛出异常并直接返回"); e.printStackTrace(); return; } log.debug("获得到锁"); LOCK.unlock(); }, "t1"); LOCK.lock(); log.debug("获得到锁"); t1.start(); Thread.sleep(1000); log.debug("主线程释放了锁"); LOCK.unlock(); } }
输出:
20:02:12.549 c.ReentrantTest [main] - 获得到锁 20:02:12.553 c.ReentrantTest [t1] - 尝试获得锁 20:02:13.555 c.ReentrantTest [t1] - 尝试获得锁超时,直接返回 20:02:13.555 c.ReentrantTest [main] - 主线程释放了锁
-
可以通过lock.tryLock(time)方法来解决哲学家就餐的死锁问题;线程在等待time后仍然没有获取到锁的话就会放弃获取锁并释放掉自己已经持有的锁:
@Slf4j(topic = "c.PhilosopherEat") public class PhilosopherEat { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } @Slf4j(topic = "c.Philosopher") class Philosopher extends Thread { final Chopstick left; final Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } @Override public void run() { while (true) { if (left.tryLock()) { try { // 如果尝试失败的话就放弃锁的获取,并且把已经持有的左筷子锁也释放掉 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } } private void eat() { log.debug("eating..."); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } class Chopstick extends ReentrantLock { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }
输出:
20:13:32.194 c.Philosopher [苏格拉底] - eating... 20:13:32.195 c.Philosopher [亚里士多德] - eating... 20:13:32.699 c.Philosopher [柏拉图] - eating... 20:13:32.699 c.Philosopher [赫拉克利特] - eating... 20:13:33.201 c.Philosopher [亚里士多德] - eating... 20:13:33.201 c.Philosopher [苏格拉底] - eating... 20:13:33.702 c.Philosopher [苏格拉底] - eating... 20:13:33.702 c.Philosopher [亚里士多德] - eating... 20:13:34.203 c.Philosopher [赫拉克利特] - eating... 20:13:34.203 c.Philosopher [柏拉图] - eating... 20:13:34.705 c.Philosopher [赫拉克利特] - eating... 20:13:34.705 c.Philosopher [苏格拉底] - eating... 20:13:35.206 c.Philosopher [苏格拉底] - eating... 20:13:35.206 c.Philosopher [赫拉克利特] - eating... 20:13:35.706 c.Philosopher [柏拉图] - eating... 20:13:35.706 c.Philosopher [阿基米德] - eating... 20:13:36.208 c.Philosopher [柏拉图] - eating... 20:13:36.208 c.Philosopher [阿基米德] - eating... 20:13:36.708 c.Philosopher [阿基米德] - eating... 20:13:36.708 c.Philosopher [柏拉图] - eating...
-
公平锁:公平锁会把竞争的线程放到一个先进先出的阻塞队列中;先进入的线程会先获得到锁,这样虽然实现了公平,但是在实际的应用中通常不会将ReentrantLock设置为公平的,因为这么做会降低并发度;想要使用公平锁的话在创建ReentrantLock的时候参数设置为true就可以了:
ReentrantLock lock = new ReentrantLock(true);
,
2. 条件变量
- 可以使用
lock.newCondition()
创建条件变量对象,并通过调用await/signal方法来实现等待/唤醒;synchronized之中也存在条件变量,就是Monitor监视器中的waitSet集合,当条件不满足的时候就是进入该集合进行等待;ReentrantLock比synchronized中的要更加的强大,因为它支持多个条件变量;就好比synchronized中不满足条件的线程会在一个休息室中等待,有条件到达的时候会把其中的所有线程都唤醒;但是ReentrantLock相当于有多个休息室,不同的条件有不同的休息室;有专门等烟的休息室;也有专门等待外卖的休息室,等等; - await方法需要在获得锁之后才能够使用;该方法在执行后会释放锁;进入conditionObject(条件变量)中等待;await在被唤醒(或被打断,超时)后会去重新竞争lock锁,竞争成功后就会从await之后继续执行;signal方法用于唤醒条件变量中的某一个等待的线程;signalAll方法唤醒所有等待的线程:
输出:@Slf4j(topic = "c.ConditionVariable") public class ConditionVariable { private static boolean hasCigarette = false; private static boolean hasTakeout = false; private static final ReentrantLock lock = new ReentrantLock(); // 等待烟的休息室 static Condition waitCigaretteSet = lock.newCondition(); // 等外卖的休息室 static Condition waitTakeoutSet = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { log.debug("有烟没?[{}]", hasCigarette); while (!hasCigarette) { log.debug("没烟,先歇会!"); try { // 此时小南进入到等烟的休息室 waitCigaretteSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("烟来咯, 可以开始干活了"); } finally { lock.unlock(); } }, "小南").start(); new Thread(() -> { lock.lock(); try { log.debug("外卖送到没?[{}]", hasTakeout); while (!hasTakeout) { log.debug("没外卖,先歇会!"); try { // 此时小女进入到等外卖的休息室 waitTakeoutSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖来咯, 可以开始干活了"); } finally { lock.unlock(); } }, "小女").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { lock.lock(); try { log.debug("送外卖的来咯~"); hasTakeout = true; // 唤醒等外卖的小女线程 waitTakeoutSet.signal(); } finally { lock.unlock(); } }, "送外卖的").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { lock.lock(); try { log.debug("送烟的来咯~"); hasCigarette = true; // 唤醒等烟的小南线程 waitCigaretteSet.signal(); } finally { lock.unlock(); } }, "送烟的").start(); } }
20:57:30.663 c.ConditionVariable [小南] - 有烟没?[false] 20:57:30.672 c.ConditionVariable [小南] - 没烟,先歇会! 20:57:30.672 c.ConditionVariable [小女] - 外卖送到没?[false] 20:57:30.672 c.ConditionVariable [小女] - 没外卖,先歇会! 20:57:31.663 c.ConditionVariable [送外卖的] - 送外卖的来咯~ 20:57:31.663 c.ConditionVariable [小女] - 外卖来咯, 可以开始干活了 20:57:32.665 c.ConditionVariable [送烟的] - 送烟的来咯~ 20:57:32.665 c.ConditionVariable [小南] - 烟来咯, 可以开始干活了
3. 各种等待唤醒API的使用
假设有两个线程,线程A打印1,线程B打印2,要求先打印2再打印1:
wait/notify实现:
@Slf4j(topic = "c.ConditionVariable")
public class ConditionVariable {
private static final Object lock = new Object();
private static boolean t2Runned = false;
public static void main(String[] args) {
final Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2Runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
final Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2Runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
输出:
21:21:23.015 c.ConditionVariable [t2] - 2
21:21:23.019 c.ConditionVariable [t1] - 1
使用await/signal:
@Slf4j(topic = "c.ConditionVariable")
public class ConditionVariable {
private static final ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static boolean t2Runned = false;
public static void main(String[] args) {
final Thread t1 = new Thread(() -> {
lock.lock();
while (!t2Runned) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
lock.unlock();
}
}, "t1");
final Thread t2 = new Thread(() -> {
lock.lock();
log.debug("2");
t2Runned = true;
condition.signal();
lock.unlock();
}, "t2");
t1.start();
t2.start();
}
}
输出:
21:29:50.162 c.ConditionVariable [t2] - 2
21:29:50.168 c.ConditionVariable [t1] - 1
使用LockSupport中的park/unpark
@Slf4j(topic = "c.ConditionVariable")
public class ConditionVariable {
public static void main(String[] args) {
final Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
}, "t2").start();
}
}
输出:
21:35:49.365 c.ConditionVariable [t2] - 2
21:35:49.368 c.ConditionVariable [t1] - 1
使用以上的各个方法也可以实现交替输出:现在想要是线程1输出a 5次;线程2输出b 5次;线程3输出c 5次;要求输出:abcabcabcabcabcabc;
wait/notify实现:
@Slf4j(topic = "guizy.TestWaitNotify")
public class TestWaitNotify {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 5);
new Thread(() -> {
waitNotify.print("a", 1, 2);
}, "a线程").start();
new Thread(() -> {
waitNotify.print("b", 2, 3);
}, "b线程").start();
new Thread(() -> {
waitNotify.print("c", 3, 1);
}, "c线程").start();
}
}
@Slf4j(topic = "guizy.WaitNotify")
@Data
@AllArgsConstructor
class WaitNotify {
private int flag;
// 循环次数
private int loopNumber;
/*
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (waitFlag != this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
this.flag = nextFlag;
this.notifyAll();
}
}
}
}
await/signal实现
@Slf4j(topic = "guizy.TestWaitNotify")
public class TestAwaitSignal {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a_condition = awaitSignal.newCondition();
Condition b_condition = awaitSignal.newCondition();
Condition c_condition = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a_condition, b_condition);
}, "a").start();
new Thread(() -> {
awaitSignal.print("b", b_condition, c_condition);
}, "b").start();
new Thread(() -> {
awaitSignal.print("c", c_condition, a_condition);
}, "c").start();
Thread.sleep(1000);
System.out.println("==========开始=========");
awaitSignal.lock();
try {
a_condition.signal(); //首先唤醒a线程
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
private final int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Condition condition, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
try {
condition.await();
//System.out.print("i:==="+i);
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
unlock();
}
}
}
}
LockSupport的park/unpark实现
@Slf4j(topic = "guizy.TestWaitNotify")
public class TestParkUnpark {
static Thread a;
static Thread b;
static Thread c;
public static void main(String[] args) {
ParkUnpark parkUnpark = new ParkUnpark(5);
a = new Thread(() -> {
parkUnpark.print("a", b);
}, "a");
b = new Thread(() -> {
parkUnpark.print("b", c);
}, "b");
c = new Thread(() -> {
parkUnpark.print("c", a);
}, "c");
a.start();
b.start();
c.start();
LockSupport.unpark(a);
}
}
class ParkUnpark {
private final int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Thread nextThread) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread);
}
}
}