线程同步
线程同步:
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个个来。
处理多线程问题时,多个线程访问同一个资源对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
- 由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题。
多线程不安全案例一:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 不安全的买票 6 * <p> 7 * 线程不安全,有可能有的人拿到负数票 8 * version: 1.0 9 */ 10 public class UnsafeBuyTicket { 11 12 public static void main(String[] args) { 13 BuyTicket station = new BuyTicket(); 14 15 new Thread(station, "我").start(); 16 new Thread(station, "你").start(); 17 new Thread(station, "黄牛党").start(); 18 } 19 } 20 21 class BuyTicket implements Runnable { 22 23 //票 24 private int ticketNums = 10; 25 boolean flag = true;//外部停止方式 26 27 @Override 28 public void run() { 29 //买票 30 while (flag) { 31 try { 32 buy(); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 private void buy() throws InterruptedException { 40 //判断是否有票 41 if (ticketNums <= 0) { 42 flag = false; 43 return; 44 } 45 //模拟延时 46 Thread.sleep(100); 47 //买票 48 System.out.println(Thread.currentThread().getName() + "拿到" + (ticketNums--)); 49 } 50 }
运行结果:
多线程不安全案例二:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 不安全取钱 6 * <p> 7 * 两个人去银行取现,账户 8 * version: 1.0 9 */ 10 public class UnsafeBank { 11 public static void main(String[] args) { 12 //账户 13 Account account = new Account(100, "结婚基金"); 14 15 Drawing you = new Drawing(account, 50, "你"); 16 Drawing girlFriend = new Drawing(account, 100, "girlFriend"); 17 18 you.start(); 19 girlFriend.start(); 20 } 21 } 22 23 //账户 24 class Account { 25 double money;//余额 26 String name;//卡名 27 28 public Account(double money, String name) { 29 this.money = money; 30 this.name = name; 31 } 32 } 33 34 //银行:模拟取款 35 class Drawing extends Thread { 36 Account account;//账户 37 //取了多少钱 38 double drawingMoney; 39 //现在手里有多少钱 40 double nowMoney; 41 42 public Drawing(Account account, double drawingMoney, String name) { 43 super(name); 44 this.account = account; 45 this.drawingMoney = drawingMoney; 46 } 47 48 //取钱 49 @Override 50 public void run() { 51 //判断有没有钱 52 if (account.money - drawingMoney < 0) { 53 System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); 54 return; 55 } 56 57 //sleep可以放大问题的发生性 58 try { 59 Thread.sleep(1000); 60 } catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 64 //卡内余额 = 余额 - 你取的钱 65 account.money = account.money - drawingMoney; 66 //你手里的钱 67 nowMoney = nowMoney + drawingMoney; 68 69 System.out.println(account.name + "余额为:" + account.money); 70 //Thread.currentThread().getName() 等价于 this.getName() 71 System.out.println(this.getName() + "手里的钱:" + nowMoney); 72 } 73 }
运行结果:
多线程不安全案例三:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * Created by 火龙裸 on 2019/11/9. 8 * desc : 线程不安全的集合 9 * version: 1.0 10 */ 11 public class UnsafeList { 12 public static void main(String[] args) { 13 14 final List<String> list = new ArrayList<>(); 15 for (int i = 0; i < 10000; i++) { 16 new Thread(new Runnable() { 17 @Override 18 public void run() { 19 list.add(Thread.currentThread().getName()); 20 } 21 }).start(); 22 } 23 24 System.out.println(list.size()); 25 } 26 }
运行结果:
可以看出,本来按照预期应该是9999,但是却只有9998。所以说ArrayList是不安全的。
为了避免消耗不必要的性能,提高效率,一般在方法里面,需要修改的内容才需要锁,锁的太多,浪费资源。比如代码中有些只读、有些只写,那么只需要对负责“只写”的那个代码块进行加锁就行。
代码示例:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 不安全的买票 6 * version: 1.0 7 */ 8 public class UnsafeBuyTicket { 9 10 public static void main(String[] args) { 11 BuyTicket station = new BuyTicket(); 12 13 new Thread(station, "我").start(); 14 new Thread(station, "你").start(); 15 new Thread(station, "黄牛党").start(); 16 } 17 } 18 19 class BuyTicket implements Runnable { 20 21 //票 22 private int ticketNums = 10; 23 boolean flag = true;//外部停止方式 24 25 @Override 26 public void run() { 27 //买票 28 while (flag) { 29 try { 30 buy(); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 } 35 } 36 37 //synchronized 同步方法,锁的是this 38 private synchronized void buy() throws InterruptedException { 39 //判断是否有票 40 if (ticketNums <= 0) { 41 flag = false; 42 return; 43 } 44 //模拟延时 45 Thread.sleep(100); 46 //买票 47 System.out.println(Thread.currentThread().getName() + "拿到" + (ticketNums--)); 48 } 49 }
运行结果:
再来第二个案例:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 不安全取钱 6 * <p> 7 * 两个人去银行取现,账户 8 * version: 1.0 9 */ 10 public class UnsafeBank { 11 public static void main(String[] args) { 12 //账户 13 Account account = new Account(100, "结婚基金"); 14 15 Drawing you = new Drawing(account, 50, "你"); 16 Drawing girlFriend = new Drawing(account, 100, "girlFriend"); 17 18 you.start(); 19 girlFriend.start(); 20 } 21 } 22 23 //账户 24 class Account { 25 double money;//余额 26 String name;//卡名 27 28 public Account(double money, String name) { 29 this.money = money; 30 this.name = name; 31 } 32 } 33 34 //银行:模拟取款 35 class Drawing extends Thread { 36 Account account;//账户 37 //取了多少钱 38 double drawingMoney; 39 //现在手里有多少钱 40 double nowMoney; 41 42 public Drawing(Account account, double drawingMoney, String name) { 43 super(name); 44 this.account = account; 45 this.drawingMoney = drawingMoney; 46 } 47 48 //取钱 49 @Override 50 public synchronized void run() { 51 //判断有没有钱 52 if (account.money - drawingMoney < 0) { 53 System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); 54 return; 55 } 56 57 //sleep可以放大问题的发生性 58 try { 59 Thread.sleep(1000); 60 } catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 64 //卡内余额 = 余额 - 你取的钱 65 account.money = account.money - drawingMoney; 66 //你手里的钱 67 nowMoney = nowMoney + drawingMoney; 68 69 System.out.println(account.name + "余额为:" + account.money); 70 //Thread.currentThread().getName() 等价于 this.getName() 71 System.out.println(this.getName() + "手里的钱:" + nowMoney); 72 } 73 }
这个代码中,synchronized添加到了run方法位置处了,很明显无效。因为这里锁run方法,synchronized默认锁的是this对象,也就是它本身,放在这里也就是锁的是Drawing对象(银行)。但是我们操作的“增删改”的对象不是银行,而是账户account对象。用account作为同步监视器,所以这个时候需要用锁代码块。把涉及到“写”操作的代码都丢到块里面就行。如下:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 安全取钱 6 * <p> 7 * 两个人去银行取现,账户 8 * version: 1.0 9 */ 10 public class UnsafeBank { 11 public static void main(String[] args) { 12 //账户 13 Account account = new Account(1000, "结婚基金"); 14 15 Drawing you = new Drawing(account, 50, "你"); 16 Drawing girlFriend = new Drawing(account, 100, "girlFriend"); 17 18 you.start(); 19 girlFriend.start(); 20 } 21 } 22 23 //账户 24 class Account { 25 double money;//余额 26 String name;//卡名 27 28 public Account(double money, String name) { 29 this.money = money; 30 this.name = name; 31 } 32 } 33 34 //银行:模拟取款 35 class Drawing extends Thread { 36 Account account;//账户 37 //取了多少钱 38 double drawingMoney; 39 //现在手里有多少钱 40 double nowMoney; 41 42 public Drawing(Account account, double drawingMoney, String name) { 43 super(name); 44 this.account = account; 45 this.drawingMoney = drawingMoney; 46 } 47 48 //取钱 49 @Override 50 public void run() { 51 //锁的对象应该是变化的量,需要增删改的对象,这里就是account。 52 synchronized (account) { 53 //判断有没有钱 54 if (account.money - drawingMoney < 0) { 55 System.out.println(Thread.currentThread().getName() + "钱不够,取不了"); 56 return; 57 } 58 59 //sleep可以放大问题的发生性 60 try { 61 Thread.sleep(1000); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 66 //卡内余额 = 余额 - 你取的钱 67 account.money = account.money - drawingMoney; 68 //你手里的钱 69 nowMoney = nowMoney + drawingMoney; 70 71 System.out.println(account.name + "余额为:" + account.money); 72 //Thread.currentThread().getName() 等价于 this.getName() 73 System.out.println(this.getName() + "手里的钱:" + nowMoney); 74 } 75 } 76 }
运行结果:
再来第三个案例:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * Created by 火龙裸 on 2019/11/9. 8 * desc : 线程不安全的集合 变成 安全 9 * version: 1.0 10 */ 11 public class UnsafeList { 12 public static void main(String[] args) { 13 14 final List<String> list = new ArrayList<>(); 15 for (int i = 0; i < 10000; i++) { 16 new Thread(new Runnable() { 17 @Override 18 public void run() { 19 //将list这个涉及到写操作的对象作为同步监视器 20 synchronized (list) { 21 list.add(Thread.currentThread().getName()); 22 } 23 } 24 }).start(); 25 } 26 27 System.out.println(list.size()); 28 } 29 }
运行结果:
小结:
死锁:
- 多个线程各自占用一些共享资源对象,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
代码示例:
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 死锁:多个线程互相抱着对方需要的资源,然后形成僵持。 6 * version: 1.0 7 */ 8 public class DeadLock { 9 public static void main(String[] args) { 10 MakeUp g1 = new MakeUp(0, "灰姑娘"); 11 MakeUp g2 = new MakeUp(1, "白雪公主"); 12 13 g1.start(); 14 g2.start(); 15 } 16 } 17 18 //口红 19 class Lipstick { 20 21 } 22 23 //镜子 24 class Mirror { 25 26 } 27 28 class MakeUp extends Thread { 29 30 //需要的资源只有一份,用static来保证只有一份 31 static Lipstick lipstick = new Lipstick(); 32 static Mirror mirror = new Mirror(); 33 34 int choice;//选择 35 String girlName;//使用化妆品的人 36 37 public MakeUp(int choice, String girlName) { 38 this.choice = choice; 39 this.girlName = girlName; 40 } 41 42 @Override 43 public void run() { 44 //化妆 45 try { 46 makeUp(); 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 } 51 52 //化妆,互相持有对象的锁,就是需要拿到对方的资源 53 private void makeUp() throws InterruptedException { 54 if (choice == 0) { 55 synchronized (lipstick) {//获得口红的锁 56 System.out.println(this.getName() + "获得口红的锁"); 57 Thread.sleep(1000); 58 59 synchronized (mirror) {//一秒钟后想获得镜子 60 System.out.println(this.getName() + "获得镜子的锁"); 61 } 62 } 63 } else { 64 synchronized (mirror) {//获得镜子的锁 65 System.out.println(this.getName() + "获得镜子的锁"); 66 Thread.sleep(2000); 67 68 synchronized (lipstick) {//一秒钟后想获得口红 69 System.out.println(this.getName() + "获得口红的锁"); 70 } 71 } 72 } 73 } 74 }
运行结果(代码中,一个人获得了口红的锁,想获得镜子的锁,一个人拿到了镜子的锁,想获得口红的锁,互相僵持。直接卡死在这个位置不动了!):
解决方式如下(直接把锁拿出来):
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 死锁:多个线程互相抱着对方需要的资源,然后形成僵持。 6 * version: 1.0 7 */ 8 public class DeadLock { 9 public static void main(String[] args) { 10 MakeUp g1 = new MakeUp(0, "灰姑娘"); 11 MakeUp g2 = new MakeUp(1, "白雪公主"); 12 13 g1.start(); 14 g2.start(); 15 } 16 } 17 18 //口红 19 class Lipstick { 20 21 } 22 23 //镜子 24 class Mirror { 25 26 } 27 28 class MakeUp extends Thread { 29 30 //需要的资源只有一份,用static来保证只有一份 31 static Lipstick lipstick = new Lipstick(); 32 static Mirror mirror = new Mirror(); 33 34 int choice;//选择 35 String girlName;//使用化妆品的人 36 37 public MakeUp(int choice, String girlName) { 38 this.choice = choice; 39 this.girlName = girlName; 40 } 41 42 @Override 43 public void run() { 44 //化妆 45 try { 46 makeUp(); 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 } 51 52 //化妆,互相持有对象的锁,就是需要拿到对方的资源 53 private void makeUp() throws InterruptedException { 54 if (choice == 0) { 55 synchronized (lipstick) {//获得口红的锁 56 System.out.println(this.getName() + "获得口红的锁"); 57 Thread.sleep(1000); 58 } 59 synchronized (mirror) {//一秒钟后想获得镜子 60 System.out.println(this.getName() + "获得镜子的锁"); 61 } 62 } else { 63 synchronized (mirror) {//获得镜子的锁 64 System.out.println(this.getName() + "获得镜子的锁"); 65 Thread.sleep(2000); 66 } 67 synchronized (lipstick) {//一秒钟后想获得口红 68 System.out.println(this.getName() + "获得口红的锁"); 69 } 70 } 71 } 72 }
运行结果:
小结:
Lock显式锁:
代码示例(不安全写法):
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 /** 4 * Created by 火龙裸 on 2019/11/9. 5 * desc : 测试Lock锁 6 * version: 1.0 7 */ 8 public class TestLock { 9 public static void main(String[] args) { 10 TestLock2 testLock2 = new TestLock2(); 11 12 new Thread(testLock2).start(); 13 new Thread(testLock2).start(); 14 new Thread(testLock2).start(); 15 } 16 } 17 18 class TestLock2 implements Runnable { 19 20 int ticketNums = 5; 21 22 @Override 23 public void run() { 24 while (true) { 25 if (ticketNums > 0) { 26 try { 27 Thread.sleep(1000); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 System.out.println(ticketNums--); 32 } else { 33 break; 34 } 35 } 36 } 37 }
运行结果:
代码示例(安全写法):
1 package com.huolongluo.coindemo.morethread.sub3; 2 3 import java.util.concurrent.locks.ReentrantLock; 4 5 /** 6 * Created by 火龙裸 on 2019/11/9. 7 * desc : 测试Lock锁 8 * version: 1.0 9 */ 10 public class TestLock { 11 public static void main(String[] args) { 12 TestLock2 testLock2 = new TestLock2(); 13 14 new Thread(testLock2).start(); 15 new Thread(testLock2).start(); 16 new Thread(testLock2).start(); 17 } 18 } 19 20 class TestLock2 implements Runnable { 21 22 int ticketNums = 5; 23 24 //定义Lock锁,Lock是一个接口,ReentrantLock实现了该接口 25 private final ReentrantLock reentrantLock = new ReentrantLock();//可重入锁 26 27 28 @Override 29 public void run() { 30 while (true) { 31 reentrantLock.lock();//加锁 32 try { 33 if (ticketNums > 0) { 34 try { 35 Thread.sleep(1000); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 System.out.println(ticketNums--); 40 } else { 41 break; 42 } 43 } finally { 44 reentrantLock.unlock();//解锁 45 } 46 } 47 } 48 }
运行结果:
总结: