线程通信、线程池、volatile关键字、synchronized
三. 等待唤醒机制
1. 线程通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
为什么要处理线程间通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
2. 等待唤醒机制
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
3. 生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。就拿存钱取钱案例来说等待唤醒机制如何有效利用资源。
案例
夫妻小明与小红有一个共同账户:共享资源。他们有三个爸爸(亲爹,干爹,岳父)给他们账户存钱。小明和小红去取钱,如果账户有钱就取出来,等待自己,然后等待自己等他们三个爸爸去存钱;爸爸们去存钱,如果发现有钱就不存,没钱就存钱,然后等待自己。
分析:
生产者线程:3个爸爸
消费者线程:小明,小红
共享资源:账户对象
账户类:
public class Account {
private String cardId;
private double money;
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public synchronized void drawMoney(int money) {
try{
String name=Thread.currentThread().getName();
if(this.money>=money){
this.money -=money;
System.out.println(name+"来取钱:"+money+",余额:"+this.money);
//等待自己唤醒别人,锁对象调用
this.notifyAll();
this.wait();
}else {
//余额不足,唤醒别人等待自己
this.notifyAll();
this.wait();
}
}catch (Exception e){
e.printStackTrace();
}
}
public synchronized void saveMoney(int money) {
try{
String name=Thread.currentThread().getName();
if(this.money> 0){
//有钱,不存,唤醒别人
this.notifyAll();
this.wait();
}else {
this.money +=money;
System.out.println(name+"来存钱:"+money+",余额:"+this.money);
this.notifyAll();
this.wait();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
存钱线程类:
public class SaveThread extends Thread{
private Account acc;
public SaveThread(Account acc, String name){
super(name);
this.acc=acc;
}
@Override
public void run() {
while (true) {
try {
this.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(100000);
}
}
}
取钱线程类:
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc=acc;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取钱
acc.drawMoney(100000);
}
}
}
主类:
public class MainTest {
public static void main(String[] args) {
//创建一个共享账户
Account acc = new Account();
//创建两个取钱线程对象代表小明和小红
new DrawThread(acc,"小明").start();
new DrawThread(acc,"小红").start();
//存钱线程
new SaveThread(acc,"亲爹").start();
new SaveThread(acc,"干爹").start();
new SaveThread(acc,"岳父").start();
}
}
结果:存取钱交替执行
总结:
- 线程通信一定多个线程操作一个资源才能通信
- 线程通信必须包装线程安全,否则无意义
四. 线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中, 有几种状态呢?在API中 java.lang.Thread.State
这个枚举中给出了六种线程状态:
五. 线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
创建与常用方法
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class ThreadPoolsTest {
public static void main(String[] args) {
//创建一个线程池
ExecutorService pools = Executors.newFixedThreadPool(3);
//提交线程任务让线程池处理
Runnable target = new MyRunnable();
pools.submit(target);//线程池创建新线程
pools.submit(target);//线程池创建新线程
pools.submit(target);//线程池创建新线程
pools.submit(target);//复用之前的线程
pools.shutdown();//等任务执行完了就会关闭线程池
// pools.shutdownNow();//立即关闭线程池,无论是否执行完毕
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++)
System.out.println(Thread.currentThread().getName()+"=>"+i);
}
}
六. 死锁
设计一个必然死锁的案例:
public class Demo {
//创建两个共享资源
public static Object resource1=new Object();
public static Object resource2=new Object();
public static void main(String[] args) {
//创建两个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource1){
System.out.println("线程1占用资源1,请求资源2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2){
System.out.println("线程1占用资源2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource2){
System.out.println("线程2占用资源2,请求资源1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1){
System.out.println("线程2占用资源2");
}
}
}
}).start();
}
}
小结:死锁代码形式上通常需要进行锁的嵌套访问
七. volatile关键字
1. 并发编程下变量不可见性问题
public class Volatiledemo extends Thread {
private boolean flag= false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
//线程中修改flag
flag =true;
System.out.println("flag =true");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
class Test{
public static void main(String[] args) {
Volatiledemo t = new Volatiledemo();
t.start();
while (true){
if (t.isFlag())
System.out.println("主线程进行");
}
}
}
运行结果:
分析:
多线程访问一个共享变量,会出现一个线程修改变量值后,其他线程看不到的情况
2. 变量不可见性的含义
本地内层与主内存的关系
问题分析:
不可见性原因:
每个线程都有自己的工作内存,线程都是从主内层拷贝共享变量的副本值,每个线程都是在自己的工作内存中操作共享变量
3.变量不可见性解决方案
3.1 加锁
每次加锁会清空自己的工作内存,重新读取主内层中的最新值
//main方法
while (true){
synchronized (Volatiledemo.class){
if (t.isFlag())
System.out.println("主线程进行");
}
}
某一个线程进入synchronized代码块前后,执行过程如下:
3.2 volatile关键字修饰
使用volatile关键字修饰该变量,一旦一个线程修改了volatile关键字修饰的变量,另一个线程立即取到最新值
private volatile boolean flag;
工作原理:
4.volatile与synchronized
八. 原子性
原子性是指一批操作是一个整体,要么同时成功,要么同时失败,不能被干扰
1. volatile的原子性
volatile只能保证线程变量的可见性,不能保证变量操作的原子性
public class Demo {
public static void main(String[] args) {
Runnable target=new MyRunnable();
for (int i=0;i<100;i++)
new Thread(target).start();
}
}
class MyRunnable implements Runnable{
private volatile int count;
@Override
public void run() {
for (int i=0;i<100;i++){
count++;
System.out.println("count="+count);
}
}
}
输出结果可能不是理想的结果(10000),结果小于10000,不具有原子性
2. 保证原子性的操作
2.1 加锁
public class Demo2 {
public static void main(String[] args) {
Runnable target=new MyRunnable2();
for (int i=0;i<100;i++)
new Thread(target).start();
}
}
class MyRunnable2 implements Runnable{
private volatile int count;
@Override
public void run() {
synchronized ("a"){
//即保证可见性,有保证原子性
for (int i=0;i<100;i++){
count++;
System.out.println("count="+count);
}
}
}
}
2.2 原子类
加锁虽然可以保证原子性,但是性能较差,于是java提供了原子类,提高了性能
AtomicInteger
原子型Inteager,可以实现原子更新操作
public class Demo3 {
public static void main(String[] args) {
Runnable target=new MyRunnable3();
for (int i=0;i<100;i++)
new Thread(target).start();
}
}
class MyRunnable3 implements Runnable{
//创建一个Integer更新的原子类AtomicInteger,初始值是0
private AtomicInteger count=new AtomicInteger();
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("count="+count.incrementAndGet());
}
}
}
3.原子类CAS机制实现线程安全
3.1 概述
3.2 CAS与Synchronized:乐观锁,悲观锁