多线程
Posted tcxpz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关的知识,希望对你有一定的参考价值。
1 线程同步
1.1 线程安全
java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有操作完共享资源前其他线程操作共享资源,从而保证该变量的唯一性和准确性。
不同步会发生什么问题?
在介绍同步方法之前先演示一下当多个线程操作同一个共享资源时可能会发生的错误。
(1)售票问题(重复票、负数票)
package com.tcxpz.syn; /** * 线程不安全: 数据有负数、相同 * @author jgp QQ:1072218420 * */ public class UnsafeTest01 { public static void main(String[] args) { //一份资源 UnsafeWeb12306 web =new UnsafeWeb12306(); //多个代理 new Thread(web,"码畜").start(); new Thread(web,"码农").start(); new Thread(web,"码蟥").start();; } }class UnsafeWeb12306 implements Runnable{ //票数 private int ticketNums =10; private boolean flag = true; @Override public void run() { while(flag) { test(); } } public void test() { if(ticketNums<0) { flag = false; return ; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } }
(2)取款问题(账户余额为负数)
package com.tcxpz.syn; /** * 线程不安全:取款 * @author jgp QQ:1072218420 * */ public class UnsafeTest02 { public static void main(String[] args) { //账户 Account account =new Account(100,"结婚礼金"); Drawing you = new Drawing(account,80,"可悲的你"); Drawing wife = new Drawing(account,90,"happy的她"); you.start(); wife.start(); } } class Account{ int money; //金额 String name; //名称 public Account(int money, String name) { this.money = money; this.name = name; } } //模拟取款 class Drawing extends Thread{ Account account ; //取钱的账户 int drawingMoney ;//取的钱数 int packetTotal ; //口袋的总数 public Drawing(Account account, int drawingMoney,String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { if(account.money -drawingMoney<0) { return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //睡眠1秒钟,两个线程都能进到这里 account.money -=drawingMoney; packetTotal +=drawingMoney; System.out.println(this.getName()+"-->账户余额为:"+account.money); System.out.println(this.getName()+"-->口袋的钱为:"+packetTotal); } }
可能的结果及分析(工作内存与主存)
Java内存模型分为主内存和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。
每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:
可能结果(多种结果,这里仅分析两种,其他类似)
result 1 (拷贝到工作内存中的数据一致)
两个进程睡眠一秒后:
1. 主内存中的account对象被拷贝到“你”和“她”的工作内存中,此时“你”的account.money和“她”的account.money均等于100;
2. “你”和“她”分别在自己的工作空间进行取钱,此时“你”的account.money=20,packetTotal=80,“她”的account.money=10,packetTotal=90;
3. “她”将“她”的account对象保存到主存中,此时主存中的account.money=10;
4. “你”将“你”的account对象保存到主存中,将之前“她”保存的的account对象覆盖掉,此时主存中的account.money=20;
5. “她”输出account.money=20,“你”输出account.money=20;
6. “她”输出packetTotal=90;“你”输出packetTotal=80。
result 2(拷贝到工作内存中的数据不一致)
两个进程睡眠一秒后:、
1. 主内存中的account对象被拷贝到“你”的工作内存中,此时“你”的account.money等于100;
2. “你”在自己的工作空间进行取钱,此时“你”的account.money=20,packetTotal=80;
3. “你”将“你”的account对象保存到主存中,将之前的account对象覆盖掉,此时主存中的account.money=20;
4. 主内存中的account对象被拷贝到“她”的工作内存中,此时“她”的account.money等于20;
5. “她”在自己的工作空间进行取钱,此时“她”的account.money=-70,packetTotal=90;
6. “她”将“她”的account对象保存到主存中,将之前的account对象覆盖掉,此时主存中的account.money=-70;
7. “你”输出account.money=-70, “她”输出account.money=-70, “你”输出packetTotal=80;“她”输出packetTotal=90。
(3)容器问题(数据丢失)
package com.tcxpz.syn; import java.util.ArrayList; import java.util.List; /** * 线程不安全:操作容器 * @author jgp QQ:1072218420 * */ public class UnsafeTest03 { public static void main(String[] args) throws InterruptedException { final List<String> list = new ArrayList<String>(); for(int i=0;i<10000;i++) { new Thread(new Runnable(){ public void run() { list.add(Thread.currentThread().getName()); } }).start(); } Thread.sleep(5000);//防止还未添加完成,主线程就已经打印 System.out.println(list.size()); } }
添加到了list的同一个位置,被覆盖。(容易导致报错)
1.2 线程同步方法
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。 存在以下问题:
- 一个线程持有锁会导致其它所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
线程同步包括两种用法:synchronized方法和 synchronized块
1.2.1 synchronized 方法
同步方法:就是用synchronize关键字修饰的方法。因为每个java对象都有一个内置锁,当用synchronize关键字修饰方法时内置锁会保护整个方法,而在调用该方法之前,要先获得内置锁,否则就会处于阻塞状态。
public synchronized void method(int args){ }
(1)售票问题(线程安全)
1 package com.tcxpz.syn; 2 /** 3 * 线程安全: 在并发时保证数据的正确性、效率尽可能高 4 * synchronized 5 * 6 * @author jgp QQ:1072218420 7 * 8 */ 9 public class SynTest01 { 10 public static void main(String[] args) { 11 //一份资源 12 SafeWeb12306 web =new SafeWeb12306(); 13 //多个代理 14 new Thread(web,"码畜").start(); 15 new Thread(web,"码农").start(); 16 new Thread(web,"码蟥").start();; 17 } 18 } 19 class SafeWeb12306 implements Runnable{ 20 //票数 21 private int ticketNums =10; 22 private boolean flag = true; 23 @Override 24 public void run() { 25 while(flag) { 26 try { 27 Thread.sleep(100); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 test(); 32 } 33 } 34 //线程安全 同步 35 public synchronized void test() { 36 if(ticketNums<=0) { 37 flag = false; 38 return ; 39 } 40 //模拟延时 41 try { 42 Thread.sleep(200); 43 } catch (InterruptedException e) { 44 e.printStackTrace(); 45 } 46 System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); 47 } 48 }
分析:通过同步方法解决了线程安全问题。同步方法的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。这里共享的资源就是web对象(第14~16行,web对象被拷贝到多个线程中),而这里的锁也是web对象(run方法在web对象中),所以线程安全。
(2)取款问题(线程不安全)
1 package com.tcxpz.syn; 2 /** 3 * 线程安全: 在并发时保证数据的正确性、效率尽可能高 4 * synchronized 5 * 1、同步方法 6 * 2、同步块 7 * @author jgp QQ:1072218420 8 */ 9 public class SynTest02 { 10 public static void main(String[] args) { 11 //账户 12 Account account =new Account(100,"结婚礼金"); 13 SafeDrawing you = new SafeDrawing(account,80,"可悲的你"); 14 SafeDrawing wife = new SafeDrawing(account,90,"happy的她"); 15 you.start(); 16 wife.start(); 17 } 18 } 19 class Account{ 20 int money; //金额 21 String name; //名称 22 public Account(int money, String name) { 23 this.money = money; 24 this.name = name; 25 } 26 } 27 //模拟取款 28 class SafeDrawing extends Thread{ 29 Account account ; //取钱的账户 30 int drawingMoney ;//取的钱数 31 int packetTotal ; //口袋的总数 32 33 public SafeDrawing(Account account, int drawingMoney,String name) { 34 super(name); 35 this.account = account; 36 this.drawingMoney = drawingMoney; 37 } 38 39 @Override 40 public void run() { 41 test(); 42 } 43 //目标不对,锁定失败。这里不应该锁this,应该锁定account 44 public synchronized void test() { 45 if(account.money -drawingMoney<0) { 46 return; 47 } 48 try { 49 Thread.sleep(1000); 50 } catch (InterruptedException e) { 51 e.printStackTrace(); 52 } 53 account.money -=drawingMoney; 54 packetTotal +=drawingMoney; 55 System.out.println(this.getName()+"-->账户余额为:"+account.money); 56 System.out.println(this.getName()+"-->口袋的钱为:"+packetTotal); 57 } 58 }
分析:同步方法没有解决线程安全问题。锁就是当前调用该方法的对象,这里不应该锁this,应该锁定account。这里共享的资源就是account对象(第13~14行,account对象被拷贝到多个线程中),而这里的锁是SafeDrawing 对象(run方法在SafeDrawing对象中),所以线程不安全。
(3)容器问题(线程安全)
package com.tcxpz.syn; import java.util.concurrent.CopyOnWriteArrayList; /** * 线程安全:操作并发容器 * @author jgp QQ:1072218420 * */ public class SynContainer { public static void main(String[] args) throws InterruptedException { final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); for(int i=0;i<10000;i++) { new Thread(new Runnable(){ @Override public void run() { list.add(Thread.currentThread().getName()); } }).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
分析:Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。CopyOnWriteArrayList的实现原理(链接)
1.2.2 synchronized块
同步代码块:就是拥有synchronize关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
synchronized (obj){ },obj称之为同步监视器
(1) 售票问题
package com.tcxpz.syn; /** * 线程安全: 在并发时保证数据的正确性、效率尽可能高 * synchronized * 1、同步方法 * 2、同步块 * * @author jgp QQ:1072218420 * */ public class SynBlockTest03 { public static void main(String[] args) { //一份资源 SynWeb12306 web =new SynWeb12306(); //多个代理 new Thread(web,"码畜").start(); new Thread(web,"码农").start(); new Thread(web,"码蟥").start();; } } class SynWeb12306 implements Runnable{ //票数 private int ticketNums =10; private boolean flag = true; @Override public void run() { while(flag) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } test5(); } } //线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性) //double checking public void test5() { if(ticketNums<=0) {//考虑的是没有票的情况 flag = false; return ; } synchronized(this) { if(ticketNums<=0) {//考虑最后的1张票 flag = false; return ; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } //线程不安全 范围太小锁不住 public void test4() { synchronized(this) { if(ticketNums<=0) { flag = false; return ; } } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } //线程不安全 ticketNums对象在变 public void test3() { synchronized((Integer)ticketNums) { if(ticketNums<=0) { flag = false; return ; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } //线程安全 范围太大 -->效率低下 public void test2() { synchronized(this) { if(ticketNums<=0) { flag = false; return ; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } //线程安全 同步 public synchronized void test1() { if(ticketNums<=0) { flag = false; return ; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } }
(2)取款问题(线程安全)
package com.tcxpz.syn; /** * 线程安全: 在并发时保证数据的正确性、效率尽可能高 * synchronized * 1、同步方法 * 2、同步块 ,目标更明确 * @author jgp QQ:1072218420 * */ public class SynBlockTest01 { public static void main(String[] args) { //账户 Account account =new Account(100,"结婚礼金"); SynDrawing you = new SynDrawing(account,80,"可悲的你"); SynDrawing wife = new SynDrawing(account,90,"happy的她"); you.start(); wife.start(); } } class Account{ int money; //金额 String name; //名称 public Account(int money, String name) { this.money = money; this.name = name; } } //模拟取款 线程安全 class SynDrawing extends Thread{ Account account ; //取钱的账户 int drawingMoney ;//取的钱数 int packetTotal ; //口袋的总数 public SynDrawing(Account account, int drawingMoney,String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { test() ; } //目标锁定account public void test() { //提高性能 if(account.money<=0) { return ; } //同步块 synchronized(account) { if(account.money -drawingMoney<0) { return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.money -=drawingMoney; packetTotal +=drawingMoney; System.out.println(this.getName()+"-->账户余额为:"+account.money); System.out.println(this.getName()+"-->口袋的钱为:"+packetTotal); } } }
(3)容器问题(线程安全)
package com.tcxpz.syn; import java.util.ArrayList; import java.util.List; /** * * 线程安全:操作容器 * @author jgp QQ:1072218420 * */ public class SynBlockTest02 { public static void main(String[] args) throws InterruptedException { final List<String> list = new ArrayList<String>(); for(int i=0;i<10000;i++) { new Thread(new Runnable(){ @Override public void run() { //同步块 synchronized(list) { list.add(Thread.currentThread().getName()); } } }).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
补充:如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象类型是Class。
操作的是哪个共享资源,就给哪个对象加锁。
1.3 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。 某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
package com.tcxpz.syn; /** * 死锁: 过多的同步可能造成相互不释放资源 * 从而相互等待,一般发生于同步中持有多个对象的锁 * * 避免: 不要在同一个代码块中,同时持有多个对象的锁 * @author jgp QQ:1072218420 * */ public class DeadLock { public static void main(String[] args) { Markup g1 = new Markup(1,"张柏芝"); Markup g2 = new Markup(0,"王菲"); g1.start(); g2.start(); } } //口红 class Lipstick{ } //镜子 class Mirror{ } //化妆 class Markup extends Thread{ static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); //选择 int choice; //名字 String girl; public Markup(int choice,String girl) { this.choice = choice; this.girl = girl; } @Override public void run() { //化妆 markup(); } //相互持有对方的对象锁-->可能造成死锁 private void markup() { if(choice==0) { synchronized(lipstick) { //获得口红的锁 System.out.println(this.girl+"涂口红"); //1秒后想拥有镜子的锁 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /* synchronized(mirror) { System.out.println(this.girl+"照镜子"); }*/ } synchronized(mirror) { System.out.println(this.girl+"照镜子"); } }else { synchronized(mirror) { //获得镜子的锁 System.out.println(this.girl+"照镜子"); //2秒后想拥有口红的锁 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } /* synchronized(lipstick) { System.out.println(this.girl+"涂口红"); } */ } synchronized(lipstick) { System.out.println(this.girl+"涂口红"); } } } }
2 线程协作
2.1 应用场景
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
2.2 分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费
- 在生产者消费者问题中,仅有synchronized是不够的,synchronized可阻止并发更新同一个共享资源,实现了同步但是不能用来实现不同线程之间的消息传递(通信)
2.3 解决方式
2.3.1 管程法
- 生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程);
- 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程);
- 缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”,生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
package com.tcxpz.cooperation; /** * 协作模型:生产者消费者实现方式一:管程法 * 借助缓冲区 * @author jgp QQ:1072218420 * */ public class CoTest01 { public static void main(String[] args) { SynContainer container = new SynContainer(); new Productor(container).start(); new Consumer(container).start(); } } //生产者 class Productor extends Thread{ SynContainer container ; public Productor(SynContainer container) { this.container = container; } public void run() { //生产 for(int i=1;i<100;i++) { System.out.println("生产第"+i+"个包子"); container.push(new Stuffedbun(i) ); } } } //消费者 class Consumer extends Thread{ SynContainer container ; public Consumer(SynContainer container) { this.container = container; } public void run() { //消费 for(int i=1;i<100;i++) { System.out.println("消费第"+container.pop().id+"个包子"); } } } //缓冲区 class SynContainer{ Stuffedbun[] buns = new Stuffedbun[10]; //存储容器 int count = 0; //计数器 //存储 生产 public synchronized void push(Stuffedbun bun) { //何时能生产 容器存在空间 //不能生产 只有等待 if(count == buns.length) { try { this.wait(); //线程阻塞 消费者通知生产解除 } catch (InterruptedException e) { } } //存在空间 可以生产 buns[count] = bun; count++; //存在数据了,可以通知消费了 this.notifyAll(); } //获取 消费 public synchronized Stuffedbun pop() { //何时消费 容器中是否存在数据 //没有数据 只有等待 if(count == 0) { try { this.wait(); //线程阻塞 生产者通知消费解除 } catch (InterruptedException e) { } } //存在数据可以消费 count --; Stuffedbun bun = buns[count] ; this.notifyAll(); //存在空间了,可以唤醒对方生产了 return bun; } } //包子 class Stuffedbun{ int id; public Stuffedbun(int id) { this.id = id; } }
2.3.2 信号灯法
package com.tcxpz.cooperation; /** * 协作模型:生产者消费者实现方式二:信号灯法 * 借助标志位 * @author jgp QQ:1072218420 * */ public class CoTest02 { public static void main(String[] args) { Kitchen kc = new Kitchen(); new Cooker(kc).start(); new Eater(kc).start(); } } //生产者 cooker class Cooker extends Thread{ Kitchen kc; public Cooker(Kitchen kc) { this.kc = kc; } public void run() { for(int i=0;i<20;i++) { if(i%2==0) { this.kc.cook("香菇青菜"); }else { this.kc.cook("牛肉粉丝"); } } } } //消费者 eater class Eater extends Thread{ Kitchen kc; public Eater(Kitchen kc) { this.kc = kc; } public void run() { for(int i=0;i<20;i++) { kc.eat(); } } } //同一个资源 厨房 class Kitchen{ String type; //信号灯 //T 表示cooker做包子 eater等待 //F 表示eater吃包子 cooker等待 boolean flag = true; //制作包子 public synchronized void cook(String type) { //eater等待 if(!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //制作 System.out.println("制作了:"+type+"包子"); this.type = type; //唤醒 this.notifyAll(); //切换标志 this.flag =!this.flag; } //饮食 public synchronized void eat() { //cooker等待 if(flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //吃包子 System.out.println("吃到了:"+type+"包子"); //唤醒 this.notifyAll(); //切换标志 this.flag =!this.flag; } }
2.4 线程通信
Java提供了3个方法解决线程之间的通信问题
生命周期:
以上是关于多线程的主要内容,如果未能解决你的问题,请参考以下文章