JUC(二)本次笔记来自狂神说和尚硅谷
8.常用的辅助类
8.1 CountDownLatch8.2 CyclicBarrier8.3 Semaphore(信号量) 9.读写锁 ReadWriteLock10.阻塞队列
10.1 什么是阻塞队列?10.2 BlockingQueue10.3 队列的四组API10.4 SynchronousQueue 同步队列 11.线程池(重点)
11.1 概念:11.2 线程池:三大方法11.3 7大参数 和 四种拒绝策略
11.3.1 源码(哪些7大参数)11.3.2 举例:11.3.3 四种拒绝策略:(java.util.concurrent) 11.4 手动创建一个线程池 12.四大函数式接口
12.1 Function 函数式接口12.2 Predicate 断定型接口12.3 Consumer 消费型接口12.4 Supplier 供给型接口 13.Stream流式计算
13.1 什么是Stream流式计算13.2 举例 14.JMM
14.1 什么是JMM(保证线程的安全)14.2 举例和8大 *** 作14.4 问题 15.Volatile
15.1 概念15.2 保证可见性15.3 不保证原子性
15.3.1 证明不保证原子性15.3.2 如何保证原子性(原子类) 15.4 禁止指令重排
15.4.1 什么是指令重排:15.4.2 volatile 16.玩转单例模式17.可重入锁(递归锁)18.死锁
18.1 什么是死锁?18.2 产生死锁原因:18.3 代码18.4 验证是否是死锁(deadlock)
8.常用的辅助类 8.1 CountDownLatchCountDownLatch 相对于是一个减法计数器,允许一个或多个线程等待直到在其他线程中执行的一组 *** 作完成的同步辅助。
import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName() + " Go out"); countDownLatch.countDown();//数量-1 },String.valueOf(i)).start(); } countDownLatch.await();//等待计数器归零,然后再向下执行 System.out.println("close Door"); } }
结果:
0 Go out
2 Go out
4 Go out
5 Go out
1 Go out
3 Go out
close Door
原理:
countDownLatch.countDown();//数量-1
countDownLatch.await();//等待计数器归零,然后再向下执行
每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行!
允许一组线程全部等待彼此达到共同屏障点的同步辅助。
CyclicBarrier 相对于是加法计数器,允许一个或多个线程等待直到在其他线程中执行的一组 *** 作完成的同步辅助。
CyclicBarrier : 指定个数线程执行完毕再执行 *** 作
代码:
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public static void main(String[] args) { //集齐七颗龙珠召唤神龙 //召换龙族的线程 CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召唤神龙成功!")); for (int i = 1; i <= 7; i++) { final int temp = i;//临时变量 //lambda 能操作到 i 吗 不可以 //lambda本质上就是个类,不可能拿到上面的变量需要转折一下》中间变量,通过final变量 new Thread(()->{ System.out.println(Thread.currentThread().getName() + "收集" + temp + "龙珠"); try { cyclicBarrier.await();//等待 // 等待7个线程执行完了,上面的计数器到7,每次循环都会加1,到7了,才会完整的走下去(开启新的线程执行) } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
结果:
Thread-0收集1龙珠
Thread-3收集4龙珠
Thread-2收集3龙珠
Thread-1收集2龙珠
Thread-4收集5龙珠
Thread-5收集6龙珠
Thread-6收集7龙珠
召唤神龙成功!
总结:
7个线程执行完了,计数器变成7,它才会完整的走下去。如果改成8,那么程序将会一直执行,停止不了,它要等到了8才会执行那句话。
注意点:
lambda 不能 *** 作到 i 。
lambda本质上就是个类,不可能拿到上面的变量
需要转折一下》中间变量,通过final变量
概念:
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。
》 semaphore 就是一个容器 用它来限制并发的线程数
Semaphore: 同一时间只能有指定数量得到线程
举例:
就比如抢车位!6车但只有3个停车位置,
第一次进来3辆,其他只能等待,有车走,那等待的车就会进来
代码:
import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreDemo { public static void main(String[] args) { //线程数量:停车位! 限流!一次性只能进来三个 Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 6; i++) { new Thread(()->{ //acquire()得到 try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "抢到车位"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + "离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release();//release()释放 //只有释放了后面的线程才会继续往里走 } },String.valueOf(i)).start(); } } }
结果:
1抢到车位
4抢到车位
2抢到车位
1离开车位
2离开车位
4离开车位
3抢到车位
6抢到车位
5抢到车位
5离开车位
3离开车位
6离开车位
注意:
不是说所有车都走才可以进
按道理是走一辆可以进一辆
这里是因为所有车都是停2秒
概念:
semaphore.acquire();
获得,假设如果已经满了,等待,等待被释放为止 !
-1的 *** 作
semaphore.release();//release()释放
释放,会将当前的信号量释放,然后唤醒等待的线程!
+ 1的 *** 作
Semaphore的作用:
多个共享资源互斥的使用!
并发限流,控制最大的线程数!
实现类: ReentrantReadWriteLock
概念:
ReadWriteLock维护一对关联的locks ,一个用于只读 *** 作,一个用于写入。
read lock可以由多个线程同时进行,write lock是独家的。
(读可以被多线程同时读,写的时候只能有一个线程去写)
作用: 提交效率和性能
举例:
写的时候只希望同时只有一个线程写,
读的时候所有人都可以读,提交性能,
这时候就可以使用ReadWriteLock
注意:
用synchronized 和lock锁可以,但是不能更精细。
下面的例子第一步不加读写锁的话
就会造成 写入的时候会有另一个插入,
达不到想要的先写入再写入ok
总结:
独占锁(写锁) 一次只能被一个线程占有
共享锁(读锁) 多个线程可以同时占有
ReadWriteLock》
读-读 可以共存!
读-写 不能共存!
写-写 不能共存!
代码:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockDemo { public static void main(String[] args) { // MyCache myCache = new MyCache(); MyCacheLock myCache = new MyCacheLock(); //写入 for (int i = 1; i <= 5; i++) { final int temp = i; new Thread(()->{ myCache.put(temp+"",temp+""); },String.valueOf(i)).start(); } //读取 for (int i = 1; i <= 5; i++) { final int temp = i; new Thread(()->{ myCache.get(temp+""); },String.valueOf(i)).start(); } } }
//1 class MyCacheLock{ private volatile Mapmap = new HashMap<>(); //读写锁:更加细粒度的控制 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //存,写入的时候,只希望同时只有一个线程写 public void put(String key,Object value){ readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "写入" + key); map.put(key,value); System.out.println(Thread.currentThread().getName() + "写入ok"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } //取,读,所有人都可以读 public void get(String key){ readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "读取" + key); Object o = map.get(key); System.out.println(Thread.currentThread().getName() + "读取ok"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } }
//2 //自定义缓存 class MyCache{ private volatile Mapmap = new HashMap<>(); // volatile保证原子性 //存,写入的时候,只希望同时只有一个线程写 public void put(String key,Object value){ System.out.println(Thread.currentThread().getName() + "写入" + key); map.put(key,value); System.out.println(Thread.currentThread().getName() + "写入ok"); } //取,读,所有人都可以读 public void get(String key){ System.out.println(Thread.currentThread().getName() + "读取" + key); Object o = map.get(key); System.out.println(Thread.currentThread().getName() + "读取ok"); } }
总结:
1.当使用MyCache myCache = new MyCache();时,即调用第二个
结果:
1写入1
1写入ok
3写入3
2写入2
…
1读取1
5写入5
2写入ok
3读取3
3读取ok
…
一看结果就知道不符合
2.当使用MyCacheLock myCache = new MyCacheLock();时,即调用第一个
结果:
1写入1
1写入ok
3写入3
3写入ok
…
2读取2
5读取5
5读取ok
1读取1
1读取ok
…
这时候就对了
双端队列: 两头都会进去 而队列只会从一头进去
10.1 什么是阻塞队列?队列》先进先出
写入: 如果队列满了,就必须阻塞等待。
取: 如果是队列是空的,必须阻塞等待生产。
10.2 BlockingQueue这两种情况属于不得不阻塞
Interface BlockingQueue
它的父接口有: Collection
实现类主要有:
ArrayBlockingQueue(数组) ,
linkedBlockingQueue(链表) ,
SynchronousQueue(同步队列)
什么情况下我们会使用阻塞队列:
多线程并发处理和线程池!
学会使用队列主要有添加、移除
" />
四组API
1、抛出异常
2、不会抛出异常(有返回值,没有异常)
3、阻塞等待(一直阻塞 一直等无止尽)
4、超时等待(等待超时 超过了某时间,我就不等了)
代码:
所有的结果都在里面
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; public class Test { public static void main(String[] args) throws InterruptedException { test3(); } //抛出异常 public static void test1(){ ArrayBlockingQueue10.4 SynchronousQueue 同步队列
概念:
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
put 存,take 取
总结:
一个线程往里面放,一个线程从里面取,一定是这个流程。
和其他的BlockingQueue不一样,SynchronousQueue不存储元素
put了一个元素,必须从里面先take取出来,否则不能在put进去值!
线程池: 三大方法、7大参数、4种拒绝策略
池化技术
线程池、连接池、内存池、对象池… 创建、销毁。十分浪费资源
程序的运行,本质:占用系统的资源!所以要进行优化资源的使用!
所以需要用到池化技术。
池化技术: 事先准备好一些资源 ,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
即线程复用、可以控制最大并发数、管理线程
注意点:
阿里巴巴java开发规范曾说过:
[强制]线程池不允许使用Executors去创建,而是通过ThreadPool Executor的方式,这样的处理方式让写的同学更加明确线程池的运行规则|规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致0OM。 (约为21亿)OOM 》内存溢出CachedThreadPool 和ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致0M。
1.newSingleThreadExecutor
》单个线程 只有一个线程在运行
2.newFixedThreadPool
》创建一个固定的线程池的大小 最多只能有xx个线程同时执行
3.newCachedThreadPool
》缓存,可伸缩的,遇强则强,遇弱则弱
代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // Executors 工具类 3大方法 public class Demo01 { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程 只有一个线程在运行 // ExecutorService threadPool = Executors.newFixedThreadPool(3); // 创建一个固定的线程池的大小 最多只能有5个线程同时执行 // ExecutorService threadPool = Executors.newCachedThreadPool(); // 缓存,可伸缩的,遇强则强,遇弱则弱 比如下面10循环 那么最高一定有10个线程同时执行 try { for (int i = 0; i < 10; i++) { //使用了线程池之后,使用线程池来创建线程 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName() + " ok"); });//一个具体的线程 } } catch (Exception e) { e.printStackTrace(); } finally { //线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
结果1:
结果2:
结果3:
本质: 上面的三大方法都是调用ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小 int maximumPoolSize, //最大核心线程池大小 long keepAliveTime, //超时了没有人调用就会释放 TimeUnit unit,//超时单位 BlockingQueue11.3.2 举例:workQueue,//阻塞队列 ThreadFactory threadFactory, //线程工厂:创建线程的,一般不用动 RejectedExecutionHandler handler//拒绝策略) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
银行办理业务,一共有五个口,空闲的时候,就开了两个,来了两个人占了两个口,第三个人就只能去候客区等待,候客区一共有三个口,假如有一天,人特别多,候客区满了,导致五个窗口必须都开,最后也都满了,这时候又来一个人,那么这个人就只能选要么走,要么等,使用的机制就是拒绝策略。
和线程池对比:
比较线程池,最开始只有两个窗口开的,这就是核心(Core)线程池的大小,五个窗口属于Max 最大线程池大小,什么时候会满,那就是候客区也满了,这时候五个窗口都会打开,即五个人同时处理业务,候客区属于阻塞队列。
1.new ThreadPoolExecutor.AbortPolicy() (默认处理的拒绝策略)
银行满了,还有人进来,不处理这个人的,
抛出异常RejectedExecutionException
2.new ThreadPoolExecutor.CallerRunsPolicy ()
哪里来的去哪里 银行不受理了 结果可能是 main ok /main线程去处理
3.new ThreadPoolExecutor.DiscardPolicy ()
队列满了,丢掉多出的任务,不会抛出异常
4.new ThreadPoolExecutor.DiscardOldestPolicy ()
队列满了,将最早进入队列的任务删除,之后再尝试加入队列,也不会抛出异常
企业里只会用这种
参数:
第一个 核心大小 2个
第二个 最大 同时只能有五个
超时等待不候:三、四
第三个 基于时间单位的数字 超过该数据就不等了
第四个 时间单位
就比如上面的银行业务,人爆满,都处理完后,五个窗口还是都开着
1个小时后还没有业务,就关闭三个窗口来休息了,即线程池要被释放了
第五个 队列 候客区
第六个 创建线程的工厂 一般是选择默认工厂
第七个 拒绝策略
import java.util.concurrent.*; public class Demo00 { public static void main(String[] args) { //自定义线程池! 工作 ThreadPoolExecutor ExecutorService threadPool = new ThreadPoolExecutor( 2, 5, 3, TimeUnit.SECONDS, new linkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); try { //最大承载:Deque + max for (int i = 1; i <= 8; i++) { //使用了线程池之后,使用线程池来参加线程 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName() + " ok"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } } }
注意:
超过9会抛异常(被拒绝策略里的) RejectedExecutionException
结果:
池的最大的大小如何去设置!
》maximumPoolSize
了解: IO密集型,CPU密集型:(调优)!
CPU密集型
最高的比如我的电脑可以12条线程同时执行
》可以保持CPU的效率最高
获取CPU的线程的代码
System.out.println(Runtime.getRuntime().availableProcessors());
IO密集型
你的程序有15个大型任务 io十分占用资源 至少有15个线程去处理io
所以一定要大于15 有多余的线程去干别的
12.四大函数式接口判断你程序中十分耗IO的线程,一般是大于2倍
lambda表达式、链式编程、函数式接口、Stream流式计算
这四个最好都会
函数式接口:只有一个方法的接口
只要是函数型接口可以用Lambda表达式简化
说明:(摘录)
匿名内部类是内部类的简化写法。
它的本质是一个带具体实现的父类或者父接口的匿名的子类对象。
一般只需要使用唯一的一次时需要用到。
匿名内部类的定义格式:
接口名称 对象名 = new 接口名称() {
// 覆盖重写所有抽象方法
};
源码:
@FunctionalInterface public interface Function{//传入参数 T 返回类型R R apply(T t); … }
Function 函数型接口,有一个输入参数,有一个输出
import java.util.function.Function; public class Demo01 { public static void main(String[] args) { // Function12.2 Predicate 断定型接口function = new Function (){ // @Override // public String apply(String str) { // return str; // } // }; Function function = (str) -> str; System.out.println(function.apply("asd"));//asd } }
源码:
@FunctionalInterface public interface Predicate{ boolean test(T t); … }
有一个输入参数,返回值只能是布尔值!
import java.util.function.Predicate; public class Demo02 { public static void main(String[] args) { //判断字符串是否为空 // Predicate12.3 Consumer 消费型接口predicate = new Predicate () { // @Override // public boolean test(String str) { // return str.isEmpty(); // } // }; Predicate predicate = str->str.isEmpty(); System.out.println(predicate.test(""));//true } }
源码:
@FunctionalInterface public interface Consumer{ void accept(T t); … }
只有输入,没有返回值
import java.util.function.Consumer; public class Demo03 { public static void main(String[] args) { // Consumer12.4 Supplier 供给型接口consumer = new Consumer () { // @Override // public void accept(String str) { // System.out.println(str); // } // }; Consumer consumer = str -> System.out.println(str); consumer.accept("asdf");//asdf } }
源码:
@FunctionalInterface public interface Supplier{ T get(); }
没有参数 只有返回值
import java.util.function.Supplier; public class Demo04 { public static void main(String[] args) { // Supplier supplier = new Supplier13.Stream流式计算 13.1 什么是Stream流式计算(){ // @Override // public Integer get() { // return 1024; // } // }; Supplier supplier = () -> {return 1024;}; System.out.println(supplier.get());//1024 } }
大数据》存储+计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来 *** 作!
User 类
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; // 无参 有参 get,set,toString方法 @Data @NoArgsConstructor @AllArgsConstructor public class User { private int id; private String name; private int age; }
题目要求:用一行代码实现!
现在有5个用户!筛选:
1、ID必须是偶数
2、年龄必须大于23岁
3、用户名转为大写字母
4、用户名字母倒着排序
5、只输出一个用户!
Test
import java.util.Arrays; import java.util.List; public class Test { public static void main(String[] args) { User u1 = new User(1,"a",21); User u2 = new User(2,"b",22); User u3 = new User(3,"C",23); User u4 = new User(4,"d",24); User u5 = new User(6,"e",25); //集合就是存储 Listlist = Arrays.asList(u1, u2, u3, u4, u5); //计算交给Stream //Lambda表达式、链式编程、函数式接口、Stream流式计算 list.stream() .filter(u->{return u.getId()%2==0;}) .filter(u->{return u.getAge()>23;}) .map(u->{return u.getName().toUpperCase();}) .sorted((uu1,uu2)->{return uu2.compareTo(uu1);}) .limit(1) .forEach(System.out::println); } }
结果 :》E
14.JMM 14.1 什么是JMM(保证线程的安全)JMM: Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
有个主存,线程A,这时候主存有个flag=true,线程A不会直接拿,而是拷贝过去到线程自己的工作内存, *** 作的是拷贝的那份,拷贝的变成false,需要刷新回主存。
8种 *** 作:(摘录)
内存交互 *** 作有8种,虚拟机实现必须保证每一个 *** 作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write *** 作在某些平台上允许例外)
lock(锁定):
作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):
作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):
作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):
作用于工作内存的变量,它把read *** 作从主存中变量放入工作内存中
use(使用):
作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign(赋值):
作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store(存储):
作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write(写入):
作用于主内存中的变量,它把store *** 作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:(摘录)
1.不允许read和load、store和write *** 作之一单独出现。即使用了read必须load,使用了store必须write
2.不允许线程丢弃他最近的assign *** 作,即工作变量的数据改变了之后,必须告知主存
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store *** 作之前,必须经过assign和load *** 作
5.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
6.如果对一个变量进行lock *** 作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign *** 作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock *** 作。也不能unlock一个被其他线程锁住的变量
8.对一个变量进行unlock *** 作之前,必须把此变量同步回主内存
这时候线程B 的flag=false刷新回主内存
主存变成flase 但线程A还是true没有读取到最新的值
问题,线程B修改了值,但是线程A不能及时可见
代码演示:
import java.util.concurrent.TimeUnit; public class Demo01 { private static int num = 0; public static void main(String[] args) {//main线程 new Thread(()->{//线程1对主内存的变化是不知道的 while (num == 0){} }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } num=1; System.out.println(num);//1 } }
》程序启动输出1,但程序不会停止
解决方法:
主内存发生变化,必须通知线程A
这时候就需要用Volatile
请你谈谈你对Volatile的理解:
Volatile是Java虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
上面的工程改成》
不加volatile 程序就会死循环!
加volatile 可以保证可见性
private volatile static int num = 0;
》结果就是1 程序也停止了
15.3 不保证原子性 15.3.1 证明不保证原子性原子性: 不可分割
线程A在执行任务的时候,不能被打扰的,
也不能被分割。要么同时成功,要么同时失败。
public class Demo02 { //volatile不保证原子性 private volatile static int num = 0; public static void add(){ num++; } //num++ 不是原子性的 *** 作 //加个synchronized 一定能解决 结果就是2万 每次保证只有一个线程进来 //加 volatile依旧不能保证原子性 还是有两个线程同时进去 public static void main(String[] args) { //理论上num结果应该为2万 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } while (Thread.activeCount()>2){//判断还有多少线程存活 >2就代表还没成功 main和gc默认执行 Thread.yield();//让出cpu使用权,重新竞争CPU执行权 } System.err.println(Thread.currentThread().getName()+" "+num); } }
结果: main 20000左右 很少会到2万
15.3.2 如何保证原子性(原子类)问题:
如果不加lock和synchronized,怎么样保证原子性
说明:
num++不是原子性的 *** 作 看似是一行代码 其实不是
查看底层源码
(底层本质上是四条字节码指令,并不是原子性 *** 作)
先找到target对应的类,然后show in Explorer打开文件夹
cmd 然后javap -c Demo02.class反编译 看到字节码文件
1.获得这个值
2.+1
3.写回这个值
解决方法:使用原子类,解决原子性问题
java.util.concurrent.atomic 原子类
(高效和少耗资源比加lock和synchronized)
常用的Classes (原子包装类)》
- Class AtomicBooleanClass AtomicInteger 更安全Class AtomicLong
代码:
import java.util.concurrent.atomic.AtomicInteger; public class Demo02 { //volatile不保证原子性 //原子类的Integer private volatile static AtomicInteger num = new AtomicInteger(); public static void add(){ num.getAndIncrement();//AtomicInteger + 1 方法 但不是个简单的+1 *** 作 用的底层的CAS 效率更高 } public static void main(String[] args) { //理论上num结果应该为2万 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } while (Thread.activeCount()>2){ Thread.yield(); } System.err.println(Thread.currentThread().getName()+" "+num); } }
结果: main 20000
15.4 禁止指令重排 15.4.1 什么是指令重排:这些类的底层都直接和 *** 作系统挂钩!在内存中修改值。
Unsafe类是一个很特殊的存在!
你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–>
指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
举例:
int x=1;//1
int y=2;//2
x=X+5; //3
y=x*x; //4
我们所期望的: 1234 但是可能执行的时候会变成2134 1324
但绝 不可能是4123!
15.4.2 volatile但指令重排出现的概率比较低
》所以加了volatile可以避免指令重排
volatile可以避免指令重排:
内存屏障。CPU指令。作用:
1、保证特定的 *** 作的执行顺序!
2、可以保证某些变量的内存可见性
( 利用这些特性volatile实现了可见性)
总结:
Volatile 是可以保持可见性。
不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
16.玩转单例模式Volatile 作用在单例模式
//懒汉式单例 public class LazyMan { private LazyMan(){ System.out.println(Thread.currentThread().getName() + "ok"); } private volatile static LazyMan lazyMan; //双重监测模式的懒汉式单例 DCL懒汉式 public static LazyMan getLazyMan() { if (lazyMan == null){ // 采用了一个double check的机制,就是每当判断如果有实例的话, // 只有在创建的时候会抢夺锁 // 其他的线程仅仅一个读取的 *** 作都需要等待锁,会造成效率低下的问题。 // 所以就是说,锁的范围越小越好 synchronized (LazyMan.class){ if (lazyMan == null){ lazyMan = new LazyMan();//不是一个原子性 *** 作 } } } return lazyMan; } //多线程并发 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getLazyMan(); }).start();//Thread-0ok } } }17.可重入锁(递归锁)
举例:
就是一个家,打开大门,
那么所有房间都可以进去
》一把锁就够了,里面的可以无障碍进入
synchronized(隐式) 上锁解锁自动完成
Lock(显式) 上锁解锁手动完成
都是可重入锁
举例1:
可重入锁 同步代码块
public class Lock01 { public static void main(String[] args) { Object o = new Object(); new Thread(()->{ synchronized (o){ System.out.println(Thread.currentThread().getName() + " 外层"); synchronized (o){ System.out.println(Thread.currentThread().getName() + " 中层"); synchronized (o){ System.out.println(Thread.currentThread().getName() + " 内层"); } } } },"t1").start(); } }
结果:
t1 外层
t1 中层
t1 内层
举例2:
可重入锁 递归调用
*/ public class Lock02 { public synchronized void add(){ add(); } public static void main(String[] args) { new Lock02().add(); } }
结果:
报栈溢出异常 》 java.lang.StackOverflowError
因为是可重入锁 体现出了循环递归调用 ,所以才会报错
举例3:
可重入锁 lock
进入第一个锁以后,无障碍进入第二个
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Lock03 { public static void main(String[] args) { Lock lock = new ReentrantLock(); new Thread(()->{ try { lock.lock(); System.out.println(Thread.currentThread().getName() + " 外层"); try { lock.lock(); System.out.println(Thread.currentThread().getName() + " 内层"); } finally { lock.unlock(); } } finally { lock.unlock(); } },"t1").start(); new Thread(()->{ lock.lock(); System.out.println("aaa"); lock.unlock(); },"aa").start(); } }
结果:
t1 外层
t1 内层
注意:
如果最里面的 lock.unlock(); 不写
虽然能出现 t1 外层 t1 内层 但是aaa就出不来 程序一直不会结束
锁没有释放,所以aa线程就得不到那把锁,
就不会执行,他要等待锁释放后,
然后得到,才能继续往下走。
两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去
18.2 产生死锁原因:第一 系统资源不足
第二 进程运行推进顺序不合适
第三 资源分配不当
import java.util.concurrent.TimeUnit; public class DeadLock { //创建两个对象 static Object a = new Object(); static Object b = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (a){ System.out.println(Thread.currentThread().getName() + " 持有锁a,试图获取锁b"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }// 看起来更明确 synchronized (b){ System.out.println(Thread.currentThread().getName() + " 获取锁b"); } } },"A").start(); new Thread(()->{ synchronized (b){ System.out.println(Thread.currentThread().getName() + " 持有锁b,试图获取锁a"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a){ System.out.println(Thread.currentThread().getName() + " 获取锁a"); } } },"B").start(); } }
结果:
A 持有锁a,试图获取锁b
B 持有锁b,试图获取锁a
》程序结束不了
第一步(jps -l) 查看对应类的进程号xxxx
jps命令 查看当前正在运行的进程和详细内容
类似linux 的ps -ef
第二步 (jstack xxxx)
jstack命令 jvm自带堆栈跟踪工具
在Terminal中写命令。
Found 1 deadlock代表出现死锁
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)