day08【线程安全、volatile关键字、原子性、并发包、死锁、线程池】
今日介绍:
(重点)"a.线程的安全
(理解)b.volatile 可见性关键字
(理解)c.原子性
(重点)"d.并发包
第一章 线程安全
1.1 线程安全问题出现的原因
a.单线程永远没有安全问题(单线程是安全的)
b.多线程同时执行,执行同样的任务,操作同一个共享数据,才有可能出现安全问题
1.2 线程安全问题的演示:卖票案例
-
代码演示
/** * 卖票任务 */ public class MyTask implements Runnable{ private int count = 100; @Override public void run() { while (true) { if (count > 0) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖出第"+count+"张票.."); count--; }else{ break; } } } } public class ThreadDemo { public static void main(String[] args) { //0.创建任务 MyTask mt = new MyTask(); //1.创建线程 Thread t1 = new Thread(mt); Thread t2 = new Thread(mt); Thread t3 = new Thread(mt); //2.启动 t1.start(); t2.start(); t3.start(); //出现的安全问题: //a.可能出现重复数据 //b.可能出现0,-1非法数据 } }
-
执行结果
a.可能出现重复数据 b.可能出现0,-1等非法数据
-
产生重复数据的原因:
- 第一个线程卖完某张票,还没有来得及让票数减1,CPU就被其他线程抢走了,导致其他线程也卖出相同的票
-
产生非法数据0和-1的原因:
- 当剩下最后一张票时,三个线程都通过大于0的判断,导致卖出第0和-1张票
1.3 线程同步
-
什么是线程同步:
让某些代码只能由一个线程进入执行,当该线程没有执行完毕之前,其他线程无法进入执行
1.4 三种线程同步的方式
-
同步代码块
格式: synchronized(锁对象){ 需要同步的代码 } 锁对象可以是任意对象,但是必须是同一个对象,也就是说锁对象,不能直接new Object(); /** * 卖票任务 */ public class MyTask implements Runnable{ private int count = 100; private Object obj = new Object(); @Override public void run() { while (true) { //同步代码块 synchronized (obj) { if (count > 0) { System.out.println(Thread.currentThread().getName() + "卖出第" + count + "张票.."); count--; } } } } }
-
同步方法
格式: public synchronized void 方法名(){ 需要同步的代码块 } /** * 卖票任务 */ public class MyTask implements Runnable{ private int count = 100; @Override public void run() { while (true) { sellTicket(); } } //同步方法 public synchronized void sellTicket() { if (count > 0) { System.out.println(Thread.currentThread().getName()+"卖出第"+count+"张票.."); count--; } } } 扩展: 同步方法能否是静态的呢??? 可以!!!如果同步方法是静态的,那么默认使用当前类的字节码文件作为锁对象
-
Lock锁机制
格式: Lock lock = new ReentrantLock(); lock.lock(); -- 加锁,获取锁 需要同步的代码 lock.unlock(); -- 解锁,释放锁 /** * 卖票任务 */ public class MyTask implements Runnable{ private int count = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { //加锁 lock.lock(); if (count > 0) { System.out.println(Thread.currentThread().getName()+"卖出第"+count+"张票.."); count--; } //解锁 lock.unlock(); } } }
第二章 volatile关键字(理解即可)
2.1. 看程序说结果
- 示例代码
public class VolatileThread extends Thread {
// 定义成员变量
private boolean flag = false ;
public boolean isFlag() { return flag;}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将flag的值更改为true
this.flag = true ;
System.out.println("flag=" + flag);
}
}
public class TestVolatileDemo {
public static void main(String[] args) {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
//主线程中的代码
while(true) {
if(volatileThread.isFlag()) {
System.out.println("执行了======");
}
}
}
}
- 结果只有一句代码输出:flag=true
2.2. JMM
- JMM是Java虚拟机提出一种Java内存模型.
- JMM中规定 所有共享变量(成员变量和静态变量) 都保存在主内存
- 当某个线程要使用该共享变量时,会在当前线程的工作内存中保存一个变量的副本
2.3. 问题分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQcHKh2H-1577451048240)(img/image-20191226102433734.png)]
2.4. 问题解决方案
-
加锁
public class TestVolatileDemo { public static void main(String[] args) { // 创建VolatileThread线程对象 VolatileThread volatileThread = new VolatileThread() ; volatileThread.start(); Object obj = new Object(); //主线程中的代码 while(true) { synchronized (obj) { // 第一种方式,加锁 if (volatileThread.isFlag()) { System.out.println("执行了======"); } } } } } 因为加锁:可以保证工作内存被清空,且从主内存获取最新的值
-
volatile关键字
public class VolatileThread extends Thread { // 定义成员变量 private volatile boolean flag = false ; //地中方式:添加volatile关键字 public boolean isFlag() { return flag;} @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 将flag的值更改为true this.flag = true ; System.out.println("flag=" + flag); } } volatile: 可见性关键字,保证当工作内存修改之后,主内存可以预见,会主动去更新最新的值 并且把所有工作内存中副本设置为无效
-
synchronized和volatile的区别
synchronized 清空工作内存,导致必须重新从主内存中获取最新的值 volatile 工作内存中的值设置为无效,不得不从内存中获取最新的值 synchronized 既可以保存可见性,也可以保证原子性 volatile 只能保证可见性,但并不具有原子性 synchronized 用于修饰方法或者代码块 volatile 用于修饰成员变量或者静态变量
第三章 原子性(理解即可)
3.1. 看程序说结果
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,1000次
for(int x = 0 ; x < 1000 ; x++) {
count++ ;//1.获取值 2.增加值 3.更新值
//count = count + 1
System.out.println("count =========>>>> " + count);
}
}
}
public class TestVolatileAtomicThreadDemo {
public static void main(String[] args) {
//1.创建任务
VolatileAtomicThread t = new VolatileAtomicThread();
//2.创建100个线程
// 开启100个线程对count进行++操作
for(int x = 0 ; x < 100 ; x++) {
new Thread(t).start();
}
}
}
结果:count =========>>>> 99998 少于 100000
3.2. 问题原理说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOscG5VP-1577451048241)(img/image-20191226110605547.png)]
3.3. volatile原子性测试
-
synchronized具有原子性,保证某个线程count++三步操作,不会分割!!
-
volatile只具有可见性,也就说当主内存的共享数据改变了,会通知所有的子线程工作内存的副本进行更新
-
但是,它不具有原子性,也就说如果我们在工作内存中对变量的操作不是原子操作,那么volatile不能保持其原子性
3.4. 问题解决方案
-
加锁
public class VolatileAtomicThread implements Runnable { // 定义一个int类型的遍历 private int count = 0; @Override public void run() { // 对该变量进行++操作,100次 for (int x = 0; x < 1000; x++) { synchronized (this) { count++; System.out.println("count =========>>>> " + count); } } } } 只要给count++加锁,那么只有获取到锁的线程执行完毕count++的所有操作之后,其他才能执行 也就是说我们count++变成原子操作
-
原子类
-
AtomicInteger原子类
-
构造方法:
public AtomicInteger();-- 默认值为0
public AtomicInteger(int initialValue);-- 指定初始值
成员方法:
public int getAndIncrement(): – 相当于 i++
public int incrementAndGet(): – 相当于 ++i
但是 getAndIncrement 和 incrementAndGet 具体原子性的
```
使用原子类改造案例
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
// private int count = 0;
private AtomicInteger count = new AtomicInteger();
@Override
public void run() {
// 对该变量进行++操作,100次
for (int x = 0; x < 1000; x++) {
count.getAndIncrement();//相当于 后++
System.out.println("count =========>>>> " + count);
}
}
}
第四章 并发包
4.1 ConcurrentHashMap
a.HashMap是线程不安全的,多个线程向同一个Map中添加键值对时,可能会出现Map中键值对个数少于实际添加的个数
b.在多线程的情况下,Java给我一个HashTable,它是线程安全的,HashTable使用全表锁
c.JDK的并发包中提出另外一个新的键值对集合:ConcurrentHashMap
他也是线程安全的,但是使用局部锁(锁当前键值对添加到的那个链表)
public class Const {
//线程不安全,最快!!!
public static HashMap<String,String> map = new HashMap<>();
//线程安全,但是由于采用全局锁(全表都加锁),所以性能较低
public static HashTable<String,String> map = new HashTable<>();
//线程安全,而且采用局部锁(只对哈希表中某个链表加锁),所以性能更高
public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
}
public class Thread1A extends Thread {
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
}
long end = System.currentTimeMillis();
System.out.println(this.getName() + " 结束!"+(end-start));
}
}
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
Thread1A a1 = new Thread1A();
Thread1A a2 = new Thread1A();
a1.setName("线程1-");
a2.setName("线程2-");
a1.start();
a2.start();
//休息10秒,确保两个线程执行完毕
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i);
}
//打印集合大小
System.out.println("Map大小:" + Const.map.size());
}
}
4.2 CountDownLatch
作用: 让一个线程,等待另外一个线程执行完毕之后,再往下执行
构造方法:
public CountDownLatch(int size); -- 其中size称为线程计数器
成员方法:
public void await(); -- 等待其他线程
public void countDown() -- 线程计数器-1
public class ThreadA extends Thread {
private CountDownLatch latch;
public ThreadA(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
System.out.println("A");
//让线程等待
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
public class ThreadB extends Thread {
private CountDownLatch latch;
public ThreadB(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
System.out.println("B");
//让线程计数器-1
latch.countDown();
}
}
public class TestDemo {
public static void main(String[] args) {
//1.创建一个CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
ThreadA a = new ThreadA(latch);
ThreadB b = new ThreadB(latch);
a.start();
b.start();
}
}
4.3 CyclicBarrier
CyclicBarrier
作用: 让一组线程都执行到某个点时,才能继续某个任务
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction);
-- parties总共需要的线程数
-- barrierAction 当所有线程都到了,需要执行的任务
成员方法:
public int await(); -- 当某个线程到了,需要调用该方法等待
需求:公司召集5名员工开会,等5名员工都到了,会议开始。
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
//0.创建一个屏障点
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("人都到了,开始开会...");
}
});
//1.创建5个线程,模拟5个员工
MyThread m1 = new MyThread(cb);
MyThread m2 = new MyThread(cb);
MyThread m3 = new MyThread(cb);
MyThread m4 = new MyThread(cb);
MyThread m5 = new MyThread(cb);
m1.setName("张三");m2.setName("李四");m3.setName("王五");
m4.setName("赵六");m5.setName("前妻");
m1.start();m2.start();m3.start();m4.start();m5.start();
}
}
public class MyThread extends Thread {
private CyclicBarrier cb;
public MyThread(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
try {
//模拟随机等待1-5秒
Thread.sleep(new Random().nextInt(5000)+1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"到了...");
//调用await方法
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
4.4 Semaphore
Semaphore
作用:是控制线程的并发数量。
构造方法:
public Semaphore(int permits); -- permits 最多允许的并发线程数量
成员方法:
public void acquire(); -- 获取线程运行的许可
public void release(); -- 归还线程运行的许可
public class MyThread extends Thread {
private Semaphore sm;
public MyThread(Semaphore sm) {
this.sm = sm;
}
@Override
public void run() {
//获取线程执行的许可
try {
sm.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "即将要执行...");
try {
Thread.sleep(new Random().nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "即将要结束...");
//归还线程的许可
sm.release();
}
}
public class TestDemo {
public static void main(String[] args) {
//0.创建一个Semaphore
Semaphore sm = new Semaphore(1);
//1.创建五个线程
for (int i = 0; i < 50; i++) {
MyThread t = new MyThread(sm);
t.start();
}
}
}
4.5 Exchanger
Exchanger
作用: 线程间数据交换器
构造:
public Exchanger()
成员方法:
public V exchange(V x); -- 将参数传给其他线程,同时接受其他线程传回的数据
public class ThreadA extends Thread{
private Exchanger exchanger;
public ThreadA(Exchanger exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程A,给线程B送礼..");
System.out.println("同时等待线程B的回礼..");
Object result = null;
try {
result = exchanger.exchange("AAAAA");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B回礼了,"+result);
}
}
public class ThreadB extends Thread{
private Exchanger exchanger;
public ThreadB(Exchanger exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程B,给线程A送礼...");
System.out.println("同时等待线程A的回礼..");
Object result = null;
try {
result = exchanger.exchange("BBBBB");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("收到了线程A的回礼,"+result);
}
}
public class TestDemo {
public static void main(String[] args) {
//1.创建一个线程间 数据交换器
Exchanger exchanger = new Exchanger();
//2.创建两个线程
ThreadA a = new ThreadA(exchanger);
ThreadB b = new ThreadB(exchanger);
a.start();
b.start();
}
}
总结:
"能够解释安全问题的出现的原因
多线程同时执行同一个任务,操作同一个共享数据,才可能出现安全问题
"能够使用同步代码块解决线程安全问题
synchronized(锁对象){
需要同步的代码
}
"能够使用同步方法解决线程安全问题
public synchronized void 方法名(){
需要同步的代码
}
注意:同步方法,需要锁,但是不需要我们手动编写,默认使用当前对象this作为锁对象
如果同步方法是静态的,默认使用当前类的字节码文件作为锁对象
"能够使用Lock锁解决线程安全问题
Lock lock = new ReentrantLock();
lock.lock();
需要同步的代码
lock.unlock();
能够说出volatile关键字的作用
可见性: 当线程的工作内存中修改了副本的值,主内存中的原本就会去更新最新的值
能够说明volatile关键字和synchronized关键字的区别
volatile 不具有原子性
synchronized 具有原子性
能够理解原子类的工作机制,CAS机制
"能够掌握原子类AtomicInteger的使用
AtomicInteger ai = new AtomicInteger(初始值);
ai.getAndIncrement(); -- 后++
ai.increamentAndGet(); -- 前++
"能够描述ConcurrentHashMap类的作用
解决HashMap线程不安全问题,解决HashTable线程安全但是性能较差问题
"能够描述CountDownLatch类的作用
用于让一个线程 等待另外一个线程执行完毕,当前线程才能继续执行
"能够描述CyclicBarrier类的作用
用于让一组线程完成某些操作之后,其他任务才能执行
"能够表述Semaphore类的作用
用于限制线程并发的最大数量
"能够描述Exchanger类的作用
用于线程间数据交换