线程同步(多个线程操作同一个资源)
核心概念
-
线程是独立的执行路径
-
在下线程运行时,即使没有自己创建线程,后台也会有多个线程,如:主线程,gc线程;
-
main()称之为主线程,为系统的入口,用于执行整个程序
-
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统相关的,先后顺序是不能人为干预的
-
对于同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
-
线程会带来额外的开销,如cpu调度时间,并发控制开销
-
每个线程在自己的工作内交互,内存控制不当会造成数据不一致
三大不安全案例
例题一:不安全抢票
package come.syn; //不安全买票 //线程不安全,有负数 public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket station = new BuyTicket(); new Thread(station,"我").start(); new Thread(station,"你").start(); new Thread(station,"黄牛").start(); } } class BuyTicket implements Runnable{ //票 private int ticketNums = 10; boolean flag = true;//外部停止方式 @Override public void run() { //买票 while (flag) { try { buy(); } catch (Exception e) { e.printStackTrace(); } } } private void buy() throws InterruptedException{ if (ticketNums<=0){ flag = false; return; } //模拟延时 Thread.sleep(100); //买票 System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--); } }
例题二:不安全的取钱
package come.syn; //不安全的取钱 //两个人去银行取钱,账户 public class UnsafeBank { public static void main(String[] args) { //账户 Account account = new Account(100,"结婚基金"); Drawing you = new Drawing(account,50,"你"); Drawing girl = new Drawing(account,100,"girl"); you.start(); girl.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 nowMoney; 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){ System.out.println(Thread.currentThread().getName()+"钱不够,去不了"); return; } //卡内余额 = 余额 - 你取得钱 account.money =account.money - drawingMoney; //你手里的钱 nowMoney = nowMoney + drawingMoney; System.out.println(account.name+"余额为:"+account.money); //Thread.currentThread().getName() = this.getName() System.out.println(this.getName()+"手里的钱"+nowMoney); } }
例题三:
package come.syn; import java.util.ArrayList; import java.util.List; public class UnsafeList { public static void main(String[] args) { List<String> list =new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
线程同步方法与同步块
-
两种用法: (synchronized方法 和 synchronized块)
-
同步方法:
1.public synchronized viod method(int args){}
2.synchronized方法控制“对象”的访问,每个 对象对应一把锁,每个synchronized方法都必须获得 调用方 法的对象的锁才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞 的线程才能获得这个锁,继续执行
3.缺点 若将一个大的方法声明为synchronized将会影响效率
-
同步块:
1.synchronized(Obj){}
2.Obj称之为同步监视器
。Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
。 同步方法中无需指定同步监视器,因为同步监视器就是this,就是这个对象本身,或者是class
3.同步监视器的执行过程
。第一个线程访问,锁定同步监视器,执行其中代码
。第二个线程访问,发现同步监视器被锁定,无法访问
。 第一个线程访问完毕,解锁同步监视器
。第二个线程访问,发现同步监视器没有锁,然后锁定并访问
CopyOnWriteArrayList
例题:测试JUC安全类集合
package come.syn; import java.util.concurrent.CopyOnWriteArrayList; //测试JUC安全类集合 public class TestJUC { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
死锁
多个线程各自占用一些共享资源,并且相互等待其他资源占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。某一个同步块同时拥有两个以上对象的锁时,就会发生“死锁”的问题。
死锁避免方法
产生死锁的四个必要条件:
-
互斥条件:一个资源每次只能被一个进程使用
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的 资源保持不放
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
-
循环条件:若干进程之间形成一种头尾相接的循环等待资源关系
上面出现的死锁的四个条件,我们只要想办法破解其中的任意一个或多个条件就可以避免死锁的发生
例题
package come.syn; //死锁:多个线程互相抱着对方的资源,然后形成僵持 public class DeadLock { public static void main(String[] args) { Makeup q1 = new Makeup(0,"甲1"); Makeup q2 = new Makeup(1,"甲2"); q1.start(); q2.start(); } } //口红 class Lipstick{ } //镜子 class Mirror{ } class Makeup extends Thread { //需要的资源只有一份,用static来保证只有一份 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choice;//选择 String girlName;//使用的人 Makeup(int choice, String girlName) { this.choice = choice; this.girlName = girlName; } @Override public void run() { //化妆 try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } //化妆,互相持有对方的锁,就是需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) {//获得口红的锁 System.out.println(this.girlName + "获得口红的锁"); Thread.sleep(1000); } synchronized (mirror) {//一秒后获得镜子 System.out.println(this.girlName + "获得镜子的锁"); } } else { synchronized (mirror) {//获得镜子的锁 System.out.println(this.girlName + "获得镜子的锁"); Thread.sleep(2000); } synchronized (lipstick) {//两秒后获得口红 System.out.println(this.girlName + "获得口红的锁"); } } } }
Lock(锁)
-
从JDK5.0开始,java提供了更强大的线程同步机制——通过显示同步定义锁对象来实现同步。同步锁使用Lock对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象
-
ReentrantLock类实现类Lock,它拥有synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLOCK,可以显示加锁,释放锁。
-
Lock代码块
class A{ private final ReentrantLock lock = new ReenTrantLock(); public void M(){ lock.lock();//加锁 try{ }finally{ lock.unlock;//解锁 //如果同步代码块有异常,要将unclock()写入finally语句块 } } }
5.synchronized与Lock对比
。Lock是显示锁(手动开启和关闭锁,别忘记关锁)synchronized是隐式锁,出了作用域自动释放
。Lock只有代码块锁,synchronized有代码块锁和方法锁
。使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并具有更好的扩展性(提供更多子类)
。 优先使用顺序
Lock > 同步代码块(已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
例题:测试Lock锁
package come.syn; import java.util.concurrent.locks.ReentrantLock; //测试Lock锁 public class TestLock { public static void main(String[] args) { TestLock2 testLock2 = new TestLock2(); new Thread(testLock2).start(); new Thread(testLock2).start(); new Thread(testLock2).start(); } } class TestLock2 implements Runnable{ int tickNums =10; //定义Lock锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try { lock.lock();//加锁 if (tickNums>0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(tickNums--); }else { break; } }finally { lock.unlock();//解锁 } } } }