多线程
线程通信
线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。
线程通信的经典模型:生产者与消费者问题。
生产者负责生成商品,消费者负责消费商品。
生产不能过剩,消费不能没有。(即时生产,即时消费)模拟一个案例:
小明和小红有一个共同账户:共享资源
他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱
他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
做整存整取:10000元。分析:
生产者线程:亲爸,岳父,干爹
消费者线程:小明,小红
共享资源:账户对象。注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。
线程通信必须先保证线程安全,否则毫无意义,代码也会报错!线程通信的核心方法:
public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用.
public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用
public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用
小结:
是一种等待唤醒机制。
必须是在同一个共享资源才需要通信,而且必须保证线程安全。
主函数
public class ThreadConnection {
public static void main(String[] args){
//1.创建共享账户
Account acc = new Account();
//2.创建线程对象
Runnable Little_Ming = new DrawThread(acc,0);
new Thread(Little_Ming,"Little_Ming").start();
Runnable Little_Hong = new DrawThread(acc,0);
new Thread(Little_Hong,"Little_Hong").start();
Runnable TrueDad = new SaveThread(acc);
new Thread(TrueDad,"True_Dad").start();
Runnable Gandad = new SaveThread(acc);
new Thread(TrueDad,"Gan_Dad").start();
Runnable GrandDad = new SaveThread(acc);
new Thread(TrueDad,"Grand_Dad").start();
}
}
/**
* 取钱线程类
*/
public class DrawThread implements Runnable{
private Account acc;
private double money;
public DrawThread(Account acc,double money){
this.acc = acc;
this.money = money;
}
@Override
public void run() {
//小明 小红取钱
while (true) {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
acc.drawMoney(10000);
}
}
}
/**
* 存钱的线程类
*/
public class SaveThread implements Runnable{
private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象!
public SaveThread(Account acc){
this.acc = acc ;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(4000);
acc.saveMoney(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//账户类
public class Account {
private String cardID;
private double money;
public Account() {
}
public synchronized void drawMoney(double money) {
try {
//谁取
String name = Thread.currentThread().getName();
//余额是否足够
if(this.money >= money){
//2.开始拿钱
this.money -= money;
System.out.println(name + "取走了" + money + ",剩余" + this.money);
//3.取钱后没钱,等待自己唤醒别人
this.notifyAll();//唤醒别人
this.wait(); //自己等待
}
}catch (Exception e){
e.printStackTrace();
}
}
public synchronized void saveMoney(double money) {
try {
//谁来存钱
String name = Thread.currentThread().getName();
//1.判断余额
if(this.money <= 0){
//没钱
this.money += money;
System.out.println(name + "来存了" + money);
}
//存完钱或者有钱,唤醒别人等待自己
this.notifyAll();
this.wait();
}catch (Exception e){
e.printStackTrace();
}
}
}
利用
this.notifyAll();
和this.wait();
轮流唤醒其他用户操作,达到线程通信的目的!!
线程状态
线性池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,
省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。为什么要用线程池:
合理利用线程池能够带来三个好处
降低资源消耗。
– 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。提高响应速度
– 不需要频繁的创建线程,如果有线程可以直接用,不会出现系统僵死!提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务。
线程池的创建
线程池在Java中的代表类:ExecutorService(接口)。
Java在Executors类下提供了一个静态方法得到一个线程池的对象:
public static ExecutorService newFixedThreadPool(int nThreads): 创建一个线程池返回。
ExecutorService提交线程任务对象执行的方法:
- Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。
小结:
pools.shutdown(); 等待任务执行完毕以后才会关闭线程池
pools.shutdownNow(); 立即关闭线程池的代码,无论任务是否执行完毕!
线程池中的线程可以被复用,线程用完以后可以继续去执行其他任务。
Runnable接口做线程池
public class ThreadPoolsDemo01 {
public static void main(String[] args){
//1.创建线程池,指定线程数量固定为3
ExecutorService pools = Executors.newFixedThreadPool(3);
//添加线程任务让线程处理
Runnable tar = new MyRunnable();
pools.submit(tar);//第一次提交任务,此时线程池创建新线程
pools.submit(tar);//第二次提交任务,此时线程池创建新线程
pools.submit(tar);//第三次提交任务,此时线程池创建新线程
pools.submit(tar);//第四次提交任务,复用之前的线程
pools.shutdown(); //等待任务执行完毕后关闭线程池
//pools.shutdownNow(); //立即关闭线程池代码,无论任务是否执行完毕!
}
}
class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 0;i < 5;i ++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}
Callable接口做线程池
线程池在Java中的代表类:ExecutorService(接口)。
Java在Executors类下提供了一个静态方法得到一个线程池的对象:
1.public static ExecutorService newFixedThreadPool(int nThreads):创建一个线程池返回。ExecutorService提交线程任务对象执行的方法:
1.Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。 1.Future<?> submit(Callable task):提交一个Runnable的任务对象给线程池执行。小结:
Callable做线程池的任务,可以得到它执行的结果!!
public class ThreadPoolsDemo02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pools = Executors.newFixedThreadPool(3);
//2.提交Callable的任务对象后返回一个未来任务对象!
Future<String> t1 = pools.submit(new MyCollable(100));
Future<String> t2 = pools.submit(new MyCollable(200));
Future<String> t3 = pools.submit(new MyCollable(300));
Future<String> t4 = pools.submit(new MyCollable(400));
//3.获取线程池执行的任务的结果
try{
String rs1 = t1.get();
String rs2 = t2.get();
String rs3 = t3.get();
String rs4 = t4.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
System.out.println(rs4);
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyCollable implements Callable<String>{
//需求:使用线程池,计算1-100,1-200,1-300的和返回
private int n;
public MyCollable(int n){
this.n = n;
}
@Override
public String call() throws Exception{
int sum = 0;
for(int i = 1;i <= n;i ++) sum += i;
return Thread.currentThread().getName() + "执行的结果为" + sum;
}
}
死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
由于线程被无限期地阻塞,因此程序不可能正常终止。客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)
java 死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,
便可让死锁消失
- 小结:
死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免。
死锁一般存在资源的嵌套请求!
public class ThreadDeadDemo01 {
//定义资源对象
public static Object resources01 = new Object();
public static Object resources02 = new Object();
public static void main(String[] args){
//死锁至少两个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources01){
System.out.println("线程1占用资源1,请求资源2");
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (resources02){
System.out.println("线程1成功占用资源2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources02){
System.out.println("线程2占用资源2,请求资源1");
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (resources01){
System.out.println("线程2成功占用资源1");
}
}
}
}).start();
}
}
volatile关键字
解决:并发编程下变量不可见性问题
引入:
- 问题:线程修改了某个成员变量的值,但是在主线程中读取到的还是之前的值
修改后的值无法读取到。- 原因:按照JMM模型,所有的成员变量和静态变量都存在于主内存中,主内存中的变量可以被多个线程共享。
每个线程都存在一个专属于自己的工作内存,工作内存一开始存储的是成员变量的副本。
所以线程很多时候都是直接访问自己工作内存中的该变量,其他线程对主内存变量值的修改将不可见!!解决此问题:
希望所有线程对于主内存的成员变量修改,其他线程是可见的。
加锁:可以实现其他线程对变量修改的可见性。
某一个线程进入synchronized代码块前后,执行过程入如下:
- 线程获得锁
- 清空工作内存
- 从主内存拷贝共享变量最新的值到工作内存成为副本
可以给成员变量加上一个volatile关键字,立即就实现了成员变量多线程修改的可见性。
小结:
- 可以给成员变量加上一个volatile关键字,当一个线程修改了这个成员变量的值,其他线程可以立即看到修改后的值并使用!
- volatile与synchronized的区别。
- volatile只能修饰实例变量和静态变量,而synchronized可以修饰方法,以及代码块。
- volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);
而synchronized是一种排他(互斥)的机制,可保证原子性(线程安全)
public class VolatileDemo01 extends Thread {
private boolean flag = false;
@Override
public void run(){
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
//线程中修改变量
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
class VisibilityDemo{
public static void main(String[] args){
//1.启动子线程,修改flag的变量成true
VolatileDemo01 var = new VolatileDemo01();
var.start();
//2.主线程
while(true){
if(var.isFlag()) System.out.println("主线程进入执行~~");
}
}
}
运行结果:
flag = true
子线程修改了值,主线程没用读到!!
不可见性解决方案
加锁
可以实现其他线程对变量修改的可见性。
某一个线程进入synchronized代码块前后,执行过程入如下:
1. 线程获得锁
2. 清空工作内存
3. 从主内存拷贝共享变量最新的值到工作内存成为副本
class VisibilityDemo01{
public static void main(String[] args){
//1.启动子线程,修改flag为true
VolatileDemo02 var = new VolatileDemo02();
var.start();
//2.主线程
while(true){
//加锁会清空工作内存,读取主内存中的最新值到工作内存中来
synchronized (VisibilityDemo01.class){
if(var.isFlag()) System.out.println("主线程进入执行~~");
}
}
}
}
Volatile关键字修饰
工作原理:一旦修改,主内存通知工作内存变量已修改,原值已失效!!再去主内存加载最新值。
private volatile boolean flag = false;
Volatile修饰变量的原子性研究
概述:所谓的原子性是指在一次操作或者多次操作中,所有的操作全部都得到了执行并且不会受到任何因素的干扰。最终结果要保证线程安全。
小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,
但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。volatile的使用场景
- 开关控制
利用可见性特点,控制某一段代码执行或者关闭。- 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。此时加上更好,其他线程可以立即读取到最新值。
volatile不能保证变量操作的原子性(安全性)。
public class VoatileDemo03 {
public static void main(String[] args){
Runnable tar = new MyRunnable();
for(int i = 1;i <= 100;i ++)
//启动100次线程,执行100次任务
new Thread(tar).start();
}
}
class MyRunnable implements Runnable{
private volatile int count = 0;
@Override
public void run(){
for(int i = 1;i <=100;i ++) {
count ++;
System.out.println("Count ==>" + count);
}
}
}
运行结果:有时不准确
加锁实现线程安全
加锁机制性能很差
class MyRunnable01 implements Runnable{
private int icount = 0;
@Override
public void run() {
synchronized ("Safty") {
for (int i = 1; i <= 100; i++) {
icount++;
System.out.println("iCount ==>" + icount);
}
}
}
}
原子类保证原子性操作
如何保证变量访问的原子性呢?
- 加锁实现线程安全。
– 虽然安全性得到了保证,但是性能不好!!- 基于CAS方式的原子类。
- Java已经提供了一些本身即可实现原子性(线程安全)的类。
- 概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单 ,性能高效,线程安全地更新一个变量的方式。
- 操作整型的原子类
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。CAS与Synchronized总结:
- CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
- Synchronized是从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!- CAS是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!
public class VolatileDemo05 {
public static void main(String[] args){
Runnable var = new MyRunnable();
for(int i = 1;i <= 100;i ++){
new Thread(var).start();
}
}
}
class MyRunnable02 implements Runnable{
//创建一个Integer更新的原子类AtomicInteger.初始值为0 取代int count
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//atomicInteger.incrementAndGet() 先加再取
System.out.println("iCount ==>" + atomicInteger.incrementAndGet());
}
}
}
原子类CAS机制
CAS:Compare And Swap(比较再交换)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。