JUC学习笔记(一):简介和锁的回顾
JUC
-
JUC其实就是指这几个util工具包
原来我们使用的:
Thread:是一个普通的线程类。
Runnable:没有返回值,效率相比Callable比较低
线程和进程 和重要概念回顾
-
进程:一个程序
-
一个进程可以包含多个线程,至少包含1个。
-
-
Java默认有2个线程:
-
main
-
GC
-
1.Java 实际上是不能开启线程的,是通过本地方法调用本地系统接口去开启的:
//Thread.start方法 public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { //调用start0方法 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } //start0是一个本地方法,调用的是底层的C++,Java无法直接操作硬件 private native void start0();
2.并发、并行
-
并发:模拟出多条线程,以时间片为单位快速的切换正在的任务。
-
并行:真正的多线程,1个处理器可以开启一个真正的线程
//获取CPU的核数 System.out.println(Runtime.getRuntime().availableProcessors());
-
并发编程的本质:充分利用CPU的资源。
线程的状态
public enum State { //新生 NEW, //运行 RUNNABLE, //阻塞 BLOCKED, //等待,一直等待 WAITING, //超时等待,超时后继续运行 TIMED_WAITING, //终止 TERMINATED; }
wait/sleep的区别
区别 | wait | sleep |
---|---|---|
来源的类 | Object | Thread |
锁 | 会释放锁 | 不会释放锁 |
使用范围 | 必须在同步代码块中 | 可以在任何地方 |
捕获异常 | 不需要捕获异常(不是中断异常) | 必须要捕获异常 |
传统的Synchronize锁
package com.rzp.demo01; import java.util.TreeMap; //基本的卖票例子 /** * 公司中多线程开发 * 线程就是一个单独的资源类,没有任何的附属操作: * 意思就是不会去继承Runnable接口,只有纯粹的方法和属性 */ public class Test1 { public static void main(String[] args) { //并发:多线程操作同一个资源类,把资源类丢入线程 Ticket ticket = new Ticket(); //使用Lamdba表达式 new Thread(()->{ for (int i = 0; i < 60; i++) { ticket.sale(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ ticket.sale(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } },"B").start(); new Thread(()->{ ticket.sale(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } },"C").start(); } } //资源类 class Ticket{ private int number = 50; public synchronized void sale(){ if (number>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余:"+number); } } }
Lock锁
Lock锁就是java.util.concurrent.Lock这个接口。
Lock锁有三个实现类:
-
ReentrantLock:可重入锁(常用)
-
ReadLock:读锁
-
WriteLock:写锁
ReentrantLock
公平锁与非公平锁
-
ReentrantLock有两个构造方法:
-
NonfairSync :非公平锁,默认构造方法,这个锁可以插队。
-
FairSync :公平锁,这个锁不能插队,必须先进先出。
-
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
使用
package com.rzp.demo01; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Demo02 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"A").start(); new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"B").start(); new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"C").start(); } } /** * lock加锁的方法: * 1. new ReentrantLock * 2.lock.lock() 加锁 * 3.finally lock.unlock() 解锁 * try代码块中的代码就会加锁了 */ class Ticket2{ private int number = 50; Lock lock = new ReentrantLock(); public void sale(){ //加锁 lock.lock(); try{ //业务代码 if (number>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余:"+number); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); //解锁 } } }
与Synchronized锁的区别
区别 | Synchronized | Lock |
---|---|---|
定义 | Java的关键字 | 类 |
是否获取到锁 | 无法判断 | 可以判断 |
释放锁 | 自动释放 | 手动释放 |
线程阻塞 | 其他线程会一直等待(blocked) | 其他线程不一定等待(waiting) |
可重入锁 | 可重入锁 | 可重入锁 |
中断 | 不可以中断 | 可以中断 |
公平 | 非公平锁 | 可以设置是否公平 |
适用范围 | 少量的代码同步问题 | 适合锁大量的同步代码 |
锁的应用:生产者消费者问题
Synchronized版解决
package com.rzp.pc; /** * * 线程之间的通信问题:生产者和消费者问题! * 线程交替执行,A B 同时操作1个变量 * 原来是使用等待和唤醒来相互通信的 */ public class A { public static void main(String[] args) { Data data = new Data(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); } } //等待,业务,通知 class Data{ private int number = 0; public synchronized void increment() throws InterruptedException { if (number!=0){ //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName()+"="+number); //通知其他线程,+1完成 this.notifyAll(); } public synchronized void decrement() throws InterruptedException { if (number == 0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"="+number); //通知其他线程,-1完毕 this.notifyAll(); } }
-
问题:
-
这个解决方案,如果有超过2个的线程时,就还是会出现并发问题。
-
-
原因:
假如有两个线程同时增加,因为使用的是if判断。
-
当前程A被唤醒的时候,就会直接执行number++,线程B也是如此,就会同时加了上去。(应该被唤醒后再判断一下,才能保证不会重复加)
-
这种应该只有1个线程被唤醒,但是实际唤醒了多个的情况,被称作虚假唤醒
-
-
解决
这时候应该把if改成while
-
因为while被唤醒后,会再次判断是否需要wait,就能避免这个问题。
-
while (number == 0){ //等待 this.wait(); }
JUC版解决
-
和synchronized对比,也是使用类似的方法。只是名称不一样了,比如:
- 在synchronized版本中,我们使用的是Object下的wait和notify方法。
- 在JUC版本中,我们用的是Lock(对应synchronized),JUC包下的Condition类的await和unlock方法。
package com.rzp.pc; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class B { public static void main(String[] args) { Data2 data = new Data2(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); } } //等待,业务,通知 class Data2 { private int number = 0; Lock lock = new ReentrantLock(); //使用lock替代了synchronized Condition condition = lock.newCondition(); //使用Condition取代了Object的Monitor(wait/notify/notifyAll方法) public void increment() throws InterruptedException { lock.lock(); //ctrl+alt+t生成try try { while (number != 0) { //等待 condition.await(); } number++; System.out.println(Thread.currentThread().getName() + "=" + number); //通知其他线程,+1完成 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decrement() throws InterruptedException { lock.lock(); try { while (number == 0) { //等待 condition.await(); } number--; System.out.println(Thread.currentThread().getName() + "=" + number); //通知其他线程,-1完毕 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
-
到这里JUC就可以实现相同的功能了,而JUC版比原来更强的地方在于,JUC可以精确唤醒线程。
指定唤醒
package com.rzp.pc; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //A执行完调用B,B执行完调用C,C执行完调用A public class C { public static void main(String[] args) { Data3 data3 = new Data3(); new Thread(() -> { for (int i = 0; i < 10; i++) data3.printA(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data3.printB(); }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) data3.printC(); }, "C").start(); } } class Data3 { private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int flag = 1; public void printA() { lock.lock(); try { while (flag != 1) { condition1.await(); } System.out.println(Thread.currentThread().getName() + "===>AAAAA"); //唤醒condition2 flag = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printB() { lock.lock(); try { while (flag != 2) { condition2.await(); } System.out.println(Thread.currentThread().getName() + "===>BBBBBB"); //唤醒condition3 flag = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printC() { lock.lock(); try { while (flag != 3) { condition3.await(); } System.out.println(Thread.currentThread().getName() + "===>CCCC"); //唤醒condition1 flag = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
8锁
-
问题:上面生产者和消费者中,锁的到底是什么东西?
-
8锁就是关于锁的8个问题
总结1:synchronized在方法中锁的是调用的对象
package com.rzp.pc; import java.util.concurrent.TimeUnit; /** * 8锁就是关于锁的8个问题 * 1.sendSms 不增加延迟4秒情况下,以下代码谁先打印: * 答案:发短信 打电话 * * 2.sendSms如果增加延迟4秒情况下,谁先打印: * 答案:发短信 打电话 */ //原因: /** * synchronized 锁在方法上,在运行时,锁的是调用这个方法的对象 * 两个方法调用的是同一个对象,因此发短信先拿到锁,所以一定是发短信先打印 * */ public class Lock8 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sendSms(); }).start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone.call(); }).start(); } } class Phone{ public synchronized void sendSms(){ //休眠4 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } }
总结2:同一个对象,没加锁的方法不需要取得锁,总是可以直接运行
package com.rzp.lock8q; import java.util.concurrent.TimeUnit; /** * 3.增加普通方法,先输出的是谁? * hello * 因为普通方法没加锁 */ public class Test2 { public static void main(String[] args) { Phone2 phone = new Phone2(); new Thread(()->{ phone.sendSms(); }).start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone.hello(); }).start(); } } class Phone2{ public synchronized void sendSms(){ //休眠4 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } public void hello(){ System.out.println("hello"); } }
package com.rzp.lock8q; import java.util.concurrent.TimeUnit; /** * 4.两个对象的时候,就是各自执行,因为是两把锁 */ public class Test2 { public static void main(String[] args) { Phone2 phone1 = new Phone2(); Phone2 phone2 = new Phone2(); new Thread(()->{ phone1.sendSms(); }).start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ phone2.call(); }).start(); } } class Phone2{ public synchronized void sendSms(){ //休眠4 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } public void hello(){ System.out.println("hello"); } }
package com.rzp.lock8q; import java.util.concurrent.TimeUnit; /** * 5.增加两个静态同步方法 */ public class Test2 { public static void main(String[] args) { //我们是直接用类名调用static方法的,而synchronized锁的是对象,这时候锁的是什么对象呢? // static 关键字:类加载的时候就加载了,加载在Phone2的class对象中。 //这种情况下,synchronized锁的实际上是class对象 new Thread(()->{ Phone2.sendSms(); }).start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ Phone2.call(); }).start(); } } class Phone2{ public static synchronized void sendSms(){ //休眠4 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public static synchronized void call(){ System.out.println("打电话"); } }
总结4.如果一个锁的是static,另一个锁的是普通方法,普通方法锁的是new出来的对象
-
这时候锁的不是一个对象,按时间顺序执行
package com.rzp.lock8q; import java.util.concurrent.TimeUnit; public class Test3 { public static void main(String[] args) { Phone4 phone4 = new Phone4(); new Thread(() -> { phone4.sendSms(); }).start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { phone4.call(); }).start(); } } class Phone4 { public static synchronized void sendSms() { //休眠4 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }