4.2 同步方法
这个博客很好,可参考:synchronized的四种用法 修饰方法 Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来
由于我们可以通过关键字 private 关键字来保证数据对象只能被方法访问,所以我们只要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种方法:
synchronized 方法和 synchronized 块.
同步方法:
public synchronized void method(int args){} • 1
- synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法申明为 synchronized 将会影响效率。
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized, synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样, 修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。 例如: 方法一 public synchronized void method() { // todo } 方法二 public void method() { synchronized(this) { // todo } } 写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的, 锁的对象都是方法的调用者,就是当前类的实例化对象
使用synchronized 同步方法 (同步方法中无需指定同步监视器,因为同步方法的同步监视器默认就是 this ,就是当前类的对象,如果是静态同步方法就是当前类Class)
package com.can.syn; //不安全的买票 //线程不安全 //"苦逼的我","牛逼的你","可恶黄牛"可能会拿到同一张票,甚至在最后一张票的时候可能都会将最后一张票执行,就会有-1 public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket buyTicket = new BuyTicket(); new Thread(buyTicket,"苦逼的我").start(); new Thread(buyTicket,"牛逼的你").start(); new Thread(buyTicket,"可恶黄牛").start(); } } class BuyTicket implements Runnable{ private int ticketNums = 10; //票 private boolean flag = true; //外部停止方式 @Override public void run() { //买票 while (flag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } //此处加油synchronized 更好的解决 线程不安全的问题 加入之后正常 private synchronized void buy() throws InterruptedException { //判断是否有票 if(ticketNums<=0){ flag = false; return; } //模拟延时 Thread.sleep(10); //买票 System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--); } }
4.3 同步块
同步块:synchronized(Obj){}
Obj 称之为同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是当前类的对象本身,如果是静态同步方法就是当前类Class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中的代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
当我们需要锁指定的对象(Account)时就需要使用同步块,我们如果使用同步方法锁,锁run()方法,默认会锁住this,即Drawing对象,这不是我们想锁的,所以我们使用同步块。
锁的是发生变化的量
package com.can.syn; import lombok.experimental.Accessors; //不安全的取钱 //两个人去银行取钱,账户 public class UnsafeBank { public static void main(String[] args) { //账户 Account account = new Account(100,"结婚基金"); Drawing you = new Drawing(account,50,"你"); Drawing girlFriend = new Drawing(account,100,"girlFriend"); you.start(); girlFriend.start(); } } //账户 class Account{ public int money; //余额 public 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() { //锁的是变化的对象。即增删改的对象 synchronized(Account){ //判断有没有钱 if(account.money-drawingMoney<0){ System.out.println(Thread.currentThread().getName()+"余额不足,取不了"); //余额不够取得钱,return return; } //模拟延时,放大问题的发生性 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //卡内余额 = 余额 - 取的钱 account.money = account.money-drawingMoney; //手里的钱 = 手里的钱 + 去的钱 nowMoney = nowMoney + drawingMoney; System.out.println(account.name+"余额为:"+account.money); //Thread.currentThread().getName() = this.getName,因为继承了Thread,所以有Thread的全部方法 System.out.println(this.getName()+"手里的钱:"+nowMoney); } } }
静态方法中的同步块
下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。
public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在 MyClass.class 这个对象上。那么这两个方法可以同时被线程访问。
4.3 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥“两个以上对象的锁”时,就可能会发生“死锁”的问题。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上述四个条件,只要破坏其任意一个就可避免死锁的发生。
死锁演示:
package com.example.democrud.democurd.test01; //死锁:多个线程互相用者对象需要的资源,互不释放相互僵持 public class DeadLock { } //口红 class Lipstick { } //镜子 class Mirror { public static void main(String[] args) { Makeup makeupA = new Makeup(0,"A女孩"); Makeup makeupB = new Makeup(1,"B女孩"); makeupA.start(); makeupB.start(); } } 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); //1s后他下拿到镜子 synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); } } } else { synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); Thread.sleep(1000); synchronized (lipstick) { System.out.println(this.girlName + "获得口红的锁"); } } } } }
此处就是死锁;他们互相拿着对方需要的东西;互补撒手;你等我 我等你;…
避免代码:
//化妆 互相持有对方的锁 private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) { System.out.println(this.girlName + "获得口红的锁"); Thread.sleep(1000); //1s后他下拿到镜子 } synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); } } else { synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); Thread.sleep(1000); } synchronized (lipstick) { System.out.println(this.girlName + "获得口红的锁"); } } }
4.4 Lock(可重入锁)
- 从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用 Lock对象充当
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显示加锁、释放锁。
synchronized 与 Lock 的对比
- Lock 是显示锁(手动开启和关闭),synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码加锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并具有更好的扩展性(提供更多的子类)
- Lock > 同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
package com.example.democrud.democurd.test01; import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) { Testlock2 testlock2 = new Testlock2(); new Thread(testlock2, "A").start(); new Thread(testlock2, "B").start(); new Thread(testlock2, "C").start(); } } class Testlock2 implements Runnable { // 10张票 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(Thread.currentThread().getName() + "的票" + tickNums--); } else { break; } } finally { lock.unlock(); //关闭锁 } } } }