2022.8.20 线程同步
6、线程同步
1.介绍
多个线程操作同一个资源
-
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
-
一个线程持有锁会导致其他所有需要此锁的线程挂起;
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
-
2.不安全的线程案例
1 package com.xing.syn; 2 3 //不安全买票 4 public class UnsafeBuyTicket { 5 public static void main(String[] args) { 6 BuyTicket buyTicket = new BuyTicket(); 7 new Thread(buyTicket, "张三").start(); 8 new Thread(buyTicket, "李四").start(); 9 new Thread(buyTicket, "王五").start(); 10 } 11 } 12 13 class BuyTicket implements Runnable { 14 //票 15 private int ticketNums = 10; 16 boolean flag = true; 17 18 @Override 19 public void run() { 20 //买票 21 while (flag) { 22 try { 23 buy(); 24 } catch (Exception e) { 25 e.printStackTrace(); 26 } 27 } 28 } 29 30 //买票 31 private void buy() { 32 //判断是否有票 33 if (ticketNums <= 0) { 34 flag = false; 35 return; 36 } 37 //延迟 38 try { 39 Thread.sleep(1); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 44 //买票 45 System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--); 46 } 47 }
剩下最后一张票时,三个人以为都有票,都去买票,没有进行线程同步,造成数据错误
1 package com.xing.syn; 2 3 /** 4 * 不安全的取钱 5 */ 6 public class UnsafeBank { 7 public static void main(String[] args) { 8 //共100 9 Account account = new Account(100, "结婚基金"); 10 11 //你取了50 12 Drawing you = new Drawing(account, 50, "you"); 13 //girlfriend取了100 14 Drawing girlfriend = new Drawing(account, 100, "girlfriend"); 15 16 //两个线程取钱 17 you.start(); 18 girlfriend.start(); 19 } 20 } 21 22 //账户 23 class Account { 24 int money;//余额 25 String cardName;//卡名 26 27 public Account(int money, String cardName) { 28 this.money = money; 29 this.cardName = cardName; 30 } 31 } 32 33 //银行:模拟取款 34 class Drawing extends Thread { 35 Account account;//账户 36 int drawingMoney;//取金额 37 int nowMoney;//你手里的钱 38 39 public Drawing(Account account, int drawingMoney, String name) { 40 super(name); 41 this.account = account; 42 this.drawingMoney = drawingMoney; 43 } 44 45 //取钱 46 @Override 47 public void run() { 48 //判断是否有钱 49 if (account.money - drawingMoney < 0) { 50 System.out.println(Thread.currentThread().getName() + "余额不足,不能进行取钱"); 51 return; 52 } 53 try { 54 Thread.sleep(1000);//放大问题的发生性 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 //卡内金额 = 余额-你取的钱 59 account.money = account.money - drawingMoney; 60 //你手里的钱 61 nowMoney = nowMoney + drawingMoney; 62 System.out.println(account.cardName + "余额为:" + account.money); 63 //this.getName()==Thread.currentThread().getName() 64 //因为继承了Thread,Thread中有getName方法 65 System.out.println(this.getName() + "手里的钱:" + nowMoney); 66 } 67 }
数据错误:剩余-50
1 package com.xing.syn; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 //线程不安全的集合 7 public class UnsafeList { 8 public static void main(String[] args) { 9 10 List<String> list = new ArrayList<String>(); 11 for (int i = 0; i < 1000; i++) { 12 new Thread(()->{ 13 //可能多个线程操作同一个list位置,数据覆盖 14 list.add(Thread.currentThread().getName()); 15 }).start(); 16 } 17 System.out.println(list.size());//正常为1000条 18 } 19 }
3.同步方法
-
由于我们可以通过private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法︰synchronized方法和synchronized 块.
同步方法: public synchronized void method(int args)
-
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷︰若将一个大的方法申明为synchronized将会影响效率
同步方法,锁的是this
实现:
1 package com.xing.syn; 2 3 //不安全买票 4 public class UnsafeBuyTicket { 5 public static void main(String[] args) { 6 BuyTicket buyTicket = new BuyTicket(); 7 new Thread(buyTicket, "张三").start(); 8 new Thread(buyTicket, "李四").start(); 9 new Thread(buyTicket, "王五").start(); 10 } 11 } 12 13 class BuyTicket implements Runnable { 14 //票 15 private int ticketNums = 10; 16 boolean flag = true; 17 18 @Override 19 public void run() { 20 //买票 21 while (flag) { 22 try { 23 buy(); 24 } catch (Exception e) { 25 e.printStackTrace(); 26 } 27 } 28 } 29 30 //买票 synchronized 同步方法 锁的是this 31 private synchronized void buy() { 32 //判断是否有票 33 if (ticketNums <= 0) { 34 flag = false; 35 return; 36 } 37 //延迟 38 try { 39 Thread.sleep(1); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 44 //买票 45 System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--); 46 } 47 }
4.同步块
-
同步块:synchronized (Obj ) { }
-
Obj称之为同步监视器
-
obj可以是任何对象,但是推荐使用共享资源作为同步监视器
-
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身,或者是class [反射中讲解]
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码.
2.第二个线程访问,发现同步监视器被锁定,无法访问.
3.第一个线程访问完毕,解锁同步监视器.
4、第二个线程访问,发现同步监视器没有锁,然后锁定并访问.
-
锁的对象就是变量的量,需要增删改查的对象
实现:
1 package com.xing.syn; 2 3 /** 4 * 不安全的取钱 5 */ 6 public class UnsafeBank { 7 public static void main(String[] args) { 8 Account1 account = new Account1(100, "结婚基金"); 9 Drawing1 you = new Drawing1(account, 50, "you"); 10 Drawing1 girlfriend = new Drawing1(account, 100, "girlfriend"); 11 you.start(); 12 girlfriend.start(); 13 } 14 } 15 16 //账户 17 class Account1 { 18 int money;//余额 19 String cardName;//卡名 20 21 public Account1(int money, String cardName) { 22 this.money = money; 23 this.cardName = cardName; 24 } 25 } 26 27 //银行:模拟取款 28 class Drawing1 extends Thread { 29 Account1 account;//账户 30 int drawingMoney;//取金额 31 int nowMoney;//你手里的钱 32 33 public Drawing1(Account1 account, int drawingMoney, String name) { 34 super(name); 35 this.account = account; 36 this.drawingMoney = drawingMoney; 37 } 38 39 //取钱 40 @Override 41 public void run() { 42 //锁的对象就是变化的量,需要增删改查的对象 43 synchronized (account) { 44 //判断是否有钱 45 if (account.money - drawingMoney < 0) { 46 System.out.println(Thread.currentThread().getName() + "余额不足,不能进行取钱"); 47 return; 48 } 49 try { 50 Thread.sleep(1000);//放大问题的发生性 51 } catch (InterruptedException e) { 52 e.printStackTrace(); 53 } 54 //卡内金额 = 余额-你的钱 55 account.money = account.money - drawingMoney; 56 //你手里的钱 57 nowMoney = nowMoney + drawingMoney; 58 System.out.println(account.cardName + "余额为:" + account.money); 59 //this.getName()==Thread.currentThread().getName() 60 System.out.println(this.getName() + "手里的钱:" + nowMoney); 61 } 62 } 63 }
1 package com.xing.syn; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class UnsafeList { 7 public static void main(String[] args) { 8 9 List<String> list = new ArrayList<String>(); 10 for (int i = 0; i < 1000; i++) { 11 new Thread(()->{ 12 synchronized (list) { 13 list.add(Thread.currentThread().getName()); 14 } 15 16 }).start(); 17 } 18 System.out.println(list.size());//1000 19 } 20 }
JUC安全集合类型扩充
1 package com.xing.syn; 2 3 import java.util.concurrent.CopyOnWriteArrayList; 4 5 //测试JUC安全类型的集合 6 public class ThreadJuc { 7 public static void main(String[] args) { 8 //这个list默认就是安全的 9 CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); 10 for (int i = 0; i < 10000; i++) { 11 new Thread(() -> { 12 list.add(Thread.currentThread().getName()); 13 }).start(); 14 } 15 try { 16 Thread.sleep(3000); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 System.out.println(list.size());//10000 21 } 22 }
5.死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
案例:
1 package com.xing.syn; 2 3 /** 4 * 死锁:多个线程互相抱着对方需要的资源,然后形成僵持 5 * 解决:一个锁只锁一个对象 6 */ 7 class Demo31_DeadLock { 8 public static void main(String[] args) { 9 Makeup makeup = new Makeup(0, "灰姑娘"); 10 Makeup makeup1 = new Makeup(1, "白雪公主"); 11 makeup.start(); 12 makeup1.start(); 13 } 14 } 15 16 //口红 17 class Lipstick { } 18 //镜子 19 class Mirror { } 20 //化妆 21 class Makeup extends Thread { 22 //需要的资源只有一份,用static保证只有一份 23 static Lipstick lipstick = new Lipstick(); 24 static Mirror mirror = new Mirror(); 25 26 int choice;//选择 27 String girlName;//使用化妆品的人 28 29 public Makeup(int choice, String girlName) { 30 this.choice = choice; 31 this.girlName = girlName; 32 } 33 34 @Override 35 public void run() { 36 //化妆 37 try { 38 makeup(); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 } 43 44 //化妆 45 private void makeup() throws InterruptedException { 46 //0代表先拿口红 非0代表先拿镜子 47 if (choice == 0) { 48 synchronized (lipstick) {//获得口红的锁 49 System.out.println(this.girlName + "获得口红的锁"); 50 Thread.sleep(1000); 51 52 synchronized (mirror) {//一秒钟后想获得镜子 53 System.out.println(this.girlName + "获得镜子的锁"); 54 } 55 } 56 } else { 57 synchronized (mirror) {//获得口红镜子 58 System.out.println(this.girlName + "获得镜子的锁"); 59 Thread.sleep(2000); 60 synchronized (lipstick) {//二秒钟后想获得的锁 61 System.out.println(this.girlName + "获得口红的锁"); 62 } 63 } 64 } 65 } 66 }
解决:
1 package com.xing.syn; 2 3 /** 4 * 死锁:多个线程互相抱着对方需要的资源,然后形成僵持 5 * 解决:一个锁只锁一个对象 6 */ 7 class Demo31_DeadLock { 8 public static void main(String[] args) { 9 Makeup makeup = new Makeup(0, "灰姑娘"); 10 Makeup makeup1 = new Makeup(1, "白雪公主"); 11 makeup.start(); 12 makeup1.start(); 13 } 14 } 15 16 //口红 17 class Lipstick { } 18 //镜子 19 class Mirror { } 20 //化妆 21 class Makeup extends Thread { 22 //需要的资源只有一份,用static保证只有一份 23 static Lipstick lipstick = new Lipstick(); 24 static Mirror mirror = new Mirror(); 25 26 int choice;//选择 27 String girlName;//使用化妆品的人 28 29 public Makeup(int choice, String girlName) { 30 this.choice = choice; 31 this.girlName = girlName; 32 } 33 34 @Override 35 public void run() { 36 //化妆 37 try { 38 makeup(); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 } 43 44 //化妆 45 private void makeup() throws InterruptedException { 46 //0代表先拿口红 非0代表先拿镜子 47 if (choice == 0) { 48 synchronized (lipstick) {//获得口红的锁 49 System.out.println(this.girlName + "获得口红的锁"); 50 Thread.sleep(1000); 51 } 52 synchronized (mirror) {//一秒钟后想获得镜子 53 System.out.println(this.girlName + "获得镜子的锁"); 54 } 55 } else { 56 synchronized (mirror) {//获得口红镜子 57 System.out.println(this.girlName + "获得镜子的锁"); 58 Thread.sleep(2000); 59 } 60 synchronized (lipstick) {//二秒钟后想获得的锁 61 System.out.println(this.girlName + "获得口红的锁"); 62 } 63 } 64 } 65 }
死锁避免办法
产生死锁的四个必要条件:
-
互斥条件:一个资源每次只能被一个进程使用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
-
循环等待条件∶若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
6.Lock(锁)
-
从JDK 5.0开始,Java提供了更强大的线程同步机制:通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
-
java.util.concurrent:locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
-
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
-
实现
1 package com.xing.syn; 2 3 import java.util.concurrent.locks.ReentrantLock; 4 5 //测试Lock锁 6 public class TestLock { 7 public static void main(String[] args) { 8 TestLock2 testLock2 = new TestLock2(); 9 10 //3个线程 11 new Thread(testLock2).start(); 12 new Thread(testLock2).start(); 13 new Thread(testLock2).start(); 14 } 15 } 16 17 class TestLock2 implements Runnable { 18 int tickerNums = 10; 19 20 //定义Lock锁 21 private final ReentrantLock lock = new ReentrantLock(); 22 23 @Override 24 public void run() { 25 while (true) { 26 try { 27 lock.lock();//加锁 28 if (tickerNums > 0) { 29 try { 30 Thread.sleep(1000); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 System.out.println(tickerNums--); 35 } else{ 36 break; 37 } 38 } finally { 39 lock.unlock();//解锁 40 } 41 } 42 } 43 }
按顺序输出,不会出现重复和-1等错误值
7.synchroized与Lock对比
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
-
优先使用顺序:
-
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
-