synchronized关键字的使用和优化
synchronized锁消除优化
-
package BingFaBianCheng.bingFaBianCheng8.shadow.test; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; @Slf4j(topic = "enjoy") public class Test7 { int i=0; public static void main(String[] args) throws InterruptedException { } public void a(){ i++; } public void b(){ Object o = new Object(); synchronized (o) { i++; } } }
-
a()方法和b()方法执行效率哪个高?
-
怎么证明哪个效率高?— jmh技术
- 可重复、可观测,执行10次,取平均结果
创建jmh项目
-
下面的命令会创建一个maven项目
-
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=com.enjoy.jmh -DartifactId=zl -Dversion=1.0.0-SNAPSHOT -DarchetypeCatalog=local
-
会报错—searching for remote catalog,下载不下来一个文件,需要手动下载
-
并把它放到你的maven仓库中— archetype-catalog.xml
-
再在上面命令的基础上加一条-DarchetypeCatalog=local 表示用本地的,不用在线下载
利用创建的jmh项目测试上面两个方法
-
需要加一个pom引用----javax.annotation
-
@Fork(1)// 只fork一个线程 @BenchmarkMode(Mode.AverageTime)// 平均时间 @Warmup(iterations=3)// 预热 @Measurement(iterations=5)// 一共执行多少次 @OutputTimeUnit(TimeUnit.NANOSECONDS)// 时间单位 public class MyBenchmark { static int i =1; @Benchmark public void a(){ i++; } // 这两个方法时间差不多 // 因为jit对代码做了优化,锁消除 // 因为这是一个局部对象,这个对象没有逃出这个方法,加锁没有任何意义 @Benchmark public void b(){ Object o = new Object(); synchronized (o) { i++; } } }
-
利用idea中maven下的package方法直接打包
-
在target下生成了两个jar包,一个项目自己的,一个benchmarks.jar
-
然后执行java -jar benchmarks.jar
-
运行a方法执行时间和运行b方法执行时间是差不多的,都是2.6纳秒左右
-
jit(即时编译器)
- i++和带有synchronized的i++方法(锁是一个局部对象),用jmh测试出来效率基本一样
- 分析
- i++这个方法一直在锁里面,
- 锁消除:以局部变量作为锁对象,jit会认为这个synchronized没有用,会去掉
测试锁消除
-
@Fork(1)// 只fork一个线程 @BenchmarkMode(Mode.AverageTime)// 平均时间 @Warmup(iterations=3)// 预热 @Measurement(iterations=5)// 一共执行多少次 @OutputTimeUnit(TimeUnit.NANOSECONDS)// 时间单位 public class MyBenchmark { static int i =1; @Benchmark public void a(){ i++; } static Object o = new Object(); @Benchmark public void b(){ synchronized (o) { i++; } } }
-
此时a方法执行时间还是2.6纳秒,b方法执行时间是31纳秒,差别在10倍以上
-
锁粗化很难证明
线程切换问题
-
@Slf4j(topic = "enjoy") public class BasicLock { public synchronized void x(){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("x"); } public synchronized void y(){ log.debug("y"); } }
-
/** * 1、等1s 打印x 打印y * 2、先打印y 等1s x */ @Slf4j(topic = "enjoy") public class TestBasic { public static void main(String[] args) { BasicLock basicLock = new BasicLock(); new Thread(()->{ log.debug("start"); basicLock.x(); },"t1").start(); new Thread(()->{ log.debug("start"); basicLock.y(); },"t2").start(); } }
-
虽然x方法睡眠了一秒钟,但是x方法和y方法到底谁先执行是随机的
静态方法和普通方法的两把锁测试
-
@Slf4j(topic = "enjoy") public class BasicLock1 { public synchronized static void x(){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("x"); } public synchronized void y(){ log.debug("y"); } public void z(){ log.debug("z"); } }
-
/** * 永远y之前 */ @Slf4j(topic = "enjoy") public class TestBasic3 { public static void main(String[] args) { BasicLock basicLock = new BasicLock(); BasicLock basicLock1 = new BasicLock(); new Thread(()->{ log.debug("start"); basicLock.x(); },"t1").start(); new Thread(()->{ log.debug("start"); basicLock1.y(); },"t2").start(); } }
-
这是两把不互斥的锁,所以不会互相影响,而x又会睡眠一秒钟,所以百分之百是y方法先执行
同一个对象,但是方法一个是静态方法,一个是普通方法
-
import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "enjoy") public class TestBasic4 { public static void main(String[] args) { BasicLock1 basicLock1 = new BasicLock1(); new Thread(()->{ log.debug("start"); basicLock1.x(); },"t1").start(); new Thread(()->{ log.debug("start"); basicLock1.y(); },"t2").start(); } }
-
x锁的是BsicLock1.class类对象
-
y锁的是this,实例对象
-
对象不对,synchronized锁不同的对象,也是彼此互不影响
synchronized基本使用
demo1
锁住实例
-
package com.shadow.demo1; import lombok.extern.slf4j.Slf4j; /** * synchronized关键字 * synchronized关键字锁定的是对象不是代码块,demo中锁的是object对象的实例 * 锁定的对象有两种:1.类的实例 2.类对象(类锁) * 加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。 */ @Slf4j(topic = "enjoy") public class Demo1 { private int count = 10; private Object object = new Object(); public void test(){ synchronized (object){ count--; log.debug(Thread.currentThread().getName() + " count = " + count); } } }
-
实际锁住的是object里面的对象头
锁住实例2
-
package com.shadow.demo1; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "enjoy") public class Demo2 { private int count = 10; public void test(){ //synchronized(this)锁定的是当前类的实例,这里锁定的是Demo2类的实例 synchronized (this){ count--; log.debug(Thread.currentThread().getName() + " count = " + count); } } }
-
此时锁住的是demo2实例的对象头
锁住方法
-
package com.shadow.demo1; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "enjoy") public class Demo3 { private int count = 10; //直接加在方法声明上,相当于是synchronized(this) public synchronized void test(){ count--; log.debug(Thread.currentThread().getName() + " count = " + count); } }
-
相当于锁住this
锁住静态方法
-
package com.shadow.demo1; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "enjoy") public class Demo4 { private static int count = 10; //synchronize关键字修饰静态方法锁定的是类的对象 public synchronized static void test(){ count--; log.debug(Thread.currentThread().getName() + " count = " + count); } public static void test2(){ synchronized (Demo4.class){ //这里不能替换成this count--; } } }
-
静态方法不能锁住this,一定要锁住静态对象Demo4.class
demo2
死循环不释放锁,导致其他线程无法执行
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo2; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * 锁对象的改变 * 锁定某对象o,如果o的属性发生改变,不影响锁的使用 * 但是如果o变成另外一个对象,则锁定的对象发生改变 * 应该避免将锁定对象的引用变成另外一个对象 */ @Slf4j(topic = "enjoy") public class Demo1 { Object o = new Object(); public void test(){ synchronized (o) { //t1 在这里无线执行 while (true) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(Thread.currentThread().getName()); } } } public static void main(String[] args) { Demo1 demo = new Demo1(); new Thread(demo :: test, "t1").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(demo :: test, "t2"); //demo.o = new Object(); //t2能否执行? t2.start(); } }
-
t2不能执行
-
但是如果更改了锁对象,此时t2也能获取到锁,因为t1一直在死循环,而相当于换了一把新锁,就锁已经失效了,所以此时t2可以获取到锁
-
因为不要轻易替换锁对象
字符串作为锁定的对象
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo2; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * 不要以字符串常量作为锁定的对象 * */ @Slf4j(topic = "enjoy") public class Demo2 { String s1 = "hello"; String s2 = "hello"; public void test1(){ synchronized (s1) { log.debug("t1 start..."); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("t1 end..."); } } public void test2(){ synchronized (s2) { log.debug("t2 start..."); } } public static void main(String[] args) { Demo2 demo = new Demo2(); //启动t1 new Thread(demo :: test1,"t1").start(); //启动t2 new Thread(demo :: test2,"t2").start(); } }
-
虽然s1和s2是两个不同的引用,实际上s1和s2是常量池里同一个对象,所以是同一把锁
-
所以尽量不要用字符串作为锁
锁的粒度
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo2; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * 同步代码快中的语句越少越好 * 比较test1和test2 * 业务逻辑中只有count++这句需要sync,这时不应该给整个方法上锁 * 采用细粒度的锁,可以使线程争用时间变短,从而提高效率 */ @Slf4j(topic = "enjoy") public class Demo3 { int count = 0; public synchronized void test1(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } count ++; try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 局部加锁 */ public void test2(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { count ++; } try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
没有必要对整个代码加锁,只应该对读写区域进行加锁
-
synchronized只对有io操作的部分加持才有,比如锁住Thread.sleep方法没有任何影响,控制锁的粒度,提高效率
demo3
多线程非同步状态造成计算错误— count–
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo3; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * 第一个线程减了个1变成9了,还没打印,第二个线程又减了个1,第三个线程又减了个1, * 这时候虽然第一个线程只减了一个1但是却打印出来一个7(这里情况是不一定的) * 可以给方法加上synchronized */ @Slf4j(topic = "enjoy") public class Demo1 implements Runnable{ private int count = 10; @SneakyThrows @Override public synchronized void run() { //TimeUnit.SECONDS.sleep(1); /** * get * -- * set */ count--; log.debug(Thread.currentThread().getName() + " count = " + count); } public static void main(String[] args) { Demo1 demo = new Demo1(); for (int i = 0; i < 5; i++) { new Thread(demo,"t" + i).start(); } } }
-
因为count–不是一个原子性操作,造成计算错误
锁不同的对象,同步锁没有起作用
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo3; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "enjoy") public class Demo2 implements Runnable{ private int count = 10; @Override public synchronized void run() { count--; log.debug(Thread.currentThread().getName() + " count = " + count); } public static void main(String[] args) { for (int i = 0; i < 5; i++) { //相比较Demo1,这里是new了五个对象,每个线程对应都拿到各自的锁标记,可以同时执行。 Demo2 demo = new Demo2(); new Thread(demo,"t" + i).start(); } } }
-
每一个线程都有去获取锁,但是获取的是不同的锁,所以没有起到线程隔离的作用
demo4
同步方法可不可以调用非同步方法
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo4; import lombok.extern.slf4j.Slf4j; //同步方法和非同步方法是否可以同时调用? 可以 @Slf4j(topic = "enjoy") public class Demo{ public synchronized void test1(){ log.debug(Thread.currentThread().getName() + " test1 start..."); try { //睡眠5s 由于还要t2要执行 cpu回去执行t2 Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(Thread.currentThread().getName() + " test1 end..."); } public void test2(){ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " test2"); } public static void main(String[] args) { Demo demo = new Demo(); //正在执行一个同步方法 没有释放锁 new Thread(demo :: test1,"t1").start(); //不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响) new Thread(demo :: test2,"t2").start(); } }
-
执行同一个类中的同步方法不会影响同一类中的非同步方法,即一个线程执行同步方法了,又来一个线程执行同一类中的非同步方法,后一个线程不会因为前一个线程而阻塞,是会同时并发执行的
-
实质上还是因为不是同一把锁,非同步方法都没有锁,所以就更不是同一把锁了
demo5
脏读问题
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo5; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * 脏读问题 * 实际业务当中应该看是否允许脏读, * 不允许的情况下对读方法也要加锁 */ @Slf4j(topic = "enjoy") public class Demo { //卡的持有人 String name; //卡上的余额 double balance; /** * * @param name * @param 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) { Demo demo = new Demo(); //2s new Thread(()->demo.set("zl",100.0)).start(); try { // 睡眠1秒钟 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //1s之后 结果 0 // 此时还有存完钱 log.debug(demo.getBalance("zl")+"");// try { // 睡眠2秒钟 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //3s之后就算100 // 此时已经存完钱了 log.debug(demo.getBalance("zl")+""); } }
-
一个人正在存,一个人正在取,会产生脏读问题,读取到了中间结果
-
项目是否允许脏读问题? — 架构师决定
-
如果不允许脏读,读取余额的时候也加锁,此时不是立马返回结果的,会转菊花,所以读是否加锁,需要看实际项目中是否允许脏读
demo6
锁的重入
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo6; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; //一个同步方法调用另外一个同步方法,能否得到锁? //重入 synchronized默认支持重入 @Slf4j(topic = "enjoy") public class Demo { synchronized void test1(){ log.debug("test1 start........."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test2(); } /** * 为什么test2还需要加sync * * 他本身就包含在test1 而test1已经加了sync */ synchronized void test2(){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("test2 start......."); } public static void main(String[] args) { Demo demo= new Demo(); demo.test1(); } }
-
为什么test2还要加synchronized? t2本身就包含在t1中,t1又加了synchronized
-
为了防止别的方法直接调用t2,而synchronized本身是支持锁重入的
demo7
锁重入的另一种情况—继承
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo7; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; //这里是重入锁的另外一种情况,继承 @Slf4j(topic = "enjoy") public class Demo { synchronized void test(){ log.debug("demo test start........"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("demo test end........"); } public static void main(String[] args) { new Demo2().test(); } } @Slf4j(topic = "enjoy") class Demo2 extends Demo { @Override synchronized void test(){ log.debug("demo2 test start........"); super.test(); log.debug("demo2 test end........"); } }
-
子类重写的父类同步方法,又再次调用父类的同步方法,是可以调用的,说明父类和子类用的是同一个锁对象,也算是一种重入
demo8
synchronized里面发生异常
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo8; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * synchronized 和异常的关系 * T2线程能否执行? */ @Slf4j(topic = "enjoy") public class Demo { Object o = new Object(); int count = 0; void test(){ synchronized(o) { //t1进入并且启动 log.debug(Thread.currentThread().getName() + " start......"); //t1 会死循环 t1 讲道理不会释放锁 while (true) { count++; log.debug(Thread.currentThread().getName() + " count = " + count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //加5次之后 发生异常 /** * 如果程序发生异常如果没有try 则会释放锁 * 反之不会释放锁 */ if (count == 5) { int i = 1 / 0; } } } } public static void main(String[] args) { Demo demo11 = new Demo(); // Runnable r = () -> demo11.test(); // new Thread(r, "t1").start(); new Thread(()->{ demo11.test(); },"t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ demo11.test(); }, "t2").start(); } }
-
线程t1中除法运算会发生异常,如果没有try-catch,会释放锁,此时t2获取到锁
-
线程t1中除法运算发生异常,如果try-catch了,此时会一直死循环,不释放锁
demo9
并发编程中最主要的问题
-
package com.shadow.demo9; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; /** * volatile 关键字,使一个变量在多个线程间可见 * mian,t1线程都用到一个变量,java默认是T1线程中保留一份副本,这样如果main线程修改了该变量, * t1线程未必知道 * * 使用volatile关键字,会让所有线程都会读到变量的修改值 * * 在下面的代码中,running是存在于堆内存的t对象中 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个副本, * 并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行 * * * 但是这可能是个错误 * 关于这个例子 在后面会专门花时间再讲 */ @Slf4j(topic = "enjoy") public class Demo { boolean running = true; List<String> list = new ArrayList<>(); /** * t1线程 */ public void test(){ log.debug("test start..."); boolean flag =running; while (running){ } log.debug("test end..."); } public static void main(String[] args) { Demo demo = new Demo(); new Thread(demo :: test,"t1").start(); try { // 主线程睡眠100毫秒,此时t1一定会被调用运行 // t1一直在死循环空转 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 修改了死循环的中断条件 // 但是仍然无法停止空转 demo.running = false; } }
-
是不是中间变量的可见性问题?
- main线程私有栈、t1线程私有栈、共享变量字段running(存在堆里面)
-
怎么终止死循环?
-
1.在while死循环加一句打印(sout)(同步方法),死循环就会停止,
-
2.定义一个全局变量list,然后在while死循环里面,list.add(xx),死循环也会停止
-
3.在共享变量前面加volatile,死循环也会停止
- volatile实际是禁止了指令重排序,跟可见性没有关系
-
-
为什么去掉共享变量的volatile就不能终止死循环了?
-
jvm自身的激进优化
-
test方法运行在t1的线程中,在死循环中每次跨栈去拿running时,jvm发现while中什么都没有做,发现没有任何意义,会对这段代码进行优化,会新增一个临时变量,把共享变量赋值给临时变量,然后先判断这个临时变量,而不是跨栈去查共享变量(effective java这本书242页)(R大)
-
boolean flag =running; if(flag){ while (running){ } }
-
java基本没有可见性问题,happens before原则已经规避了可见性问题
-
这个问题的优化是在机器码这个级别解决(指令级别优化),不是在class文件级别
-
demo10
volatile不能解决原子性,只能保证可见性
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo10; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; /** * 比如说第一个线程加到100了,还没往上加,另外一个线程来了,把100拿过来执行方法, * 然后第一个线程继续加到101,第二个线程也加到101,他两往回写都是101,线程不会管你加到哪儿了, * 虽然说加了2但是实际上只加了1. * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题, * 也就是说volatile不能替代synchronized或者说volatile保证不了原子性 */ @Slf4j(topic = "enjoy") public class Demo { volatile int count = 0; public void test(){ for (int i = 0; i < 10000; i++) { count ++; } } public static void main(String[] args) { Demo demo = new Demo(); List<Thread> threads = new ArrayList(); //new 10個线程 for (int i = 0; i < 10; i++) { threads.add(new Thread(demo::test, "t-" + i)); } //遍历这个10个线程 依次启动 threads.forEach((o)->o.start()); //等待10个线程执行完 threads.forEach((o)->{ try { o.join(); } catch (Exception e) { e.printStackTrace(); } }); log.debug(demo.count+""); } }
-
count++不是原子性操作,有3步操作,get 、++、set
-
volatile替代synchronized,synchronized可以同时保证可见性和原子性
demo11
synchronized既保证了原子性又保证了可见性
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo11; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j(topic = "enjoy") public class Demo { int count = 0; //相比较上一个例子,synchronized既保证了原子性又保证了可见性 public synchronized void test(){ for (int i = 0; i < 10000; i++) { count ++; } } public static void main(String[] args) { Demo demo = new Demo(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(demo::test, "thread-" + i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch (Exception e) { e.printStackTrace(); } }); log.debug(demo.count+""); } }
demo12
AtomicInteger
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo12; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * atomicXXX * 一道面试题:多个atomic类连续调用能否构成原子性? */ @Slf4j(topic = "enjoy") public class Demo { AtomicInteger count = new AtomicInteger(0); public void test(){ for (int i = 0; i < 10000; i++) { if(count.get() < 1000){ //count++ count.incrementAndGet(); } } } public static void main(String[] args) { Demo demo = new Demo(); List<Thread> threads = new ArrayList(); for (int i = 0; i < 10; i++) { threads.add(new Thread(demo::test, "thread-" + i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch (Exception e) { e.printStackTrace(); } }); log.debug(demo.count+""); } }
-
单个atomic操作是原子性的
- count.incrementAndGet(),是原子性的count++
-
多个atomic操作不是原子性的
-
if(count.get() < 1000){ //count++ count.incrementAndGet(); }
-
此时计算结果还是可能有问题,因为还是有线程切换
-
-
这种多原子操作的,只能通过加锁来解决
demo13
线程安全的list
-
package com.shadow.demo13; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 一道面试题:实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数, * 当个数到5个时,线程2给出提示并结束线程2 * * 这里list在两个线程之间不保证可见性,所以线程2始终结束不了 */ @Slf4j(topic = "enjoy") public class Container1 { List lists = new ArrayList(); public void add(Object o){ lists.add(o); } public int size(){ return lists.size(); } public static void main(String[] args) { Container1 c = new Container1(); new Thread(()->{ for (int i = 0; i < 10; i++) { c.add(new Object()); log.debug("add " + i); try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } } }," t1").start(); new Thread(()->{ while (true) { if (c.size() == 5) { break; } } log.debug("t2线程结束"); }, "t2").start(); } }
-
此时t2压根就不会结束,
改进list–加volatile
-
package com.shadow.demo13; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 一道面试题:实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数, * 当个数到5个时,线程2给出提示并结束 * * 有两个问题,第一由于没有加同步,可能size等于5的时候,有另外一个线程加了一下才break,不是很精确 * 第二个问题就是浪费cpu,T2线程用的是死循环 */ @Slf4j(topic = "enjoy") public class Container2 { 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) { Container2 c = new Container2(); new Thread(()->{ for (int i = 0; i < 10; i++) { c.add(new Object()); log.debug("add " + i); try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } } }," t1").start(); /** * 浪费性能 * 当条件不满足的我应该放弃CPU 阻塞 * 当条件满足的时候才执行 wait notify */ new Thread(()->{ while (true) { if (c.size() == 5) { break; } } log.debug("t2线程结束"); }, "t2").start(); } }
-
理论还是存在问题,如果不睡眠,就会更容易出现,假设当list的size加到5个了,t2执行c.size==5的判断时,时间片又给t1了,并且又加了1个,此时再切换为线程t2时,已经不满足了,完美错过,还会是死循环
-
这个问题在多核电脑上很难复现,单核电脑上则很容易复现
-
最大的问题还是浪费性能,一直死循环
持续优化,当条件不满足时放弃cpu,而不是空转浪费cpu
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo13; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 一道面试题:实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时, * 线程2给出提示并结束 * * 这里虽然T2能够及时收到消息唤醒,但是wait会释放锁,notify不会释放锁,所以T1线程结束后 * T2线程才执行完成 */ @Slf4j(topic = "enjoy") public class Container3 { 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) { Container3 c = new Container3(); Object lock = new Object(); new Thread(()->{ synchronized (lock) { log.debug("t2启动"); if (c.size() != 5) { try { lock.wait(); } catch (Exception e) { e.printStackTrace(); } } log.debug("t2结束"); } }," t2").start(); new Thread(()->{ log.debug("t1启动"); synchronized (lock) { for (int i = 0; i < 10; i++) { c.add(new Object()); log.debug("add " + i); if (c.size() == 5) { lock.notify(); } try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } } } }, "t1").start(); } }
-
wait会释放锁,notify不会释放锁
-
t2被唤醒后,会进入entryList去抢锁,但是此时t1都没有释放锁,所以t2拿不到锁,一直要到t1释放锁后才能执行
t1再次wait释放锁
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo13; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 一道面试题:实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数, * 当个数到5个时,线程2给出提示并结束 * * 相比较上一个例子,这里T1里面用wait释放锁,T2能够及时结束 */ @Slf4j(topic = "enjoy") public class Container4 { 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) { Container4 c = new Container4(); Object lock = new Object(); new Thread(()->{ synchronized (lock) { log.debug("t2启动"); while (c.size() < 5) { try { lock.wait(); } catch (Exception e) { e.printStackTrace(); } } log.debug("t2结束"); // 再次唤醒t1 lock.notify(); } }," t2").start(); new Thread(()->{ log.debug("t1启动"); synchronized (lock) { for (int i = 0; i < 10; i++) { c.add(new Object()); log.debug("add " + i); if (c.size() == 5) { lock.notify(); try { lock.wait();//要释放锁,T2才能得到锁得以执行 } catch (Exception e) { e.printStackTrace(); } } try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } } } }, "t1").start(); } }
两个线程来回wait-notify,很别扭,有没有更优的方法
-
package BingFaBianCheng.bingFaBianCheng8.shadow.demo13; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 一道面试题:实现一个容器,提供两个方法,add,size * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数, * 当个数到5个时,线程2给出提示并结束 * * CountDownLatch * 使用await和countdown方法替代wait和notify * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行 * 相当于是发令枪,运动员线程调用await等待,计数到0开始运行 * 当不涉及同步,只是涉及线程通信的时候,用synchronized加wait,notify就显得太重了 */ @Slf4j(topic = "enjoy") public class Container5 { 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) { Container5 c = new Container5(); CountDownLatch latch = new CountDownLatch(1); String s = new String("XXXXX"); new Thread(()->{ log.debug("t2启动"); try { //阻塞 latch.await();//准备 } catch (Exception e) { e.printStackTrace(); } log.debug("t2结束"); }," t2").start(); new Thread(()->{ log.debug("t1启动"); for (int i = 0; i < 10; i++) { c.add(new Object()); log.debug("add " + i); if (c.size() == 5) { // 因为countDownLatch等于1 // 释放一次后就等于0了 latch.countDown(); } try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } } }, "t1").start(); } }
-
只有当CountDownLatch等于0的时候,会自动把所有因为countDownLatch阻塞的线程全部唤醒,所以此时t2就会继续执行,否则就阻塞