线程同步

  • 多个线程操作同一个资源
  • 并发:同一个对象被多个线程同时操作
  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

队列和锁

  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突,为了保证数据在方法中被访问的正确性,在访问时加入 锁机制 synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可
  • 存在以下问题:
  • 一个线程持有锁会导致其他所有需要此所得线程挂起
  • 在多线程竞争下,加锁,释放锁会倒置比较多得上下文切换和调度延时,引起性能问题
  • 如果一个优先级高得线程等待一个优先级低得线程释放锁会导致优先级倒置,引起性能问题
  • 线程不安全例子:
  • 由于没有加入锁机制,会出现几个线程同时拿到一张票的情况
package Thread;
public class UnsafeBuyTic {
    public static void main(String[] args) {
        Buy_tic test=new Buy_tic();
        Thread p1=new Thread(test,"p1");
        Thread p2=new Thread(test,"p2");
        Thread p3=new Thread(test,"p3");
        p1.start();
        p2.start();
        p3.start();
    }
}
class Buy_tic implements Runnable{
    private int ticket=10;
    boolean flag=true;
    @Override
    public void run() {
        while (flag) {
            try {
                bbuy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private void bbuy() throws InterruptedException {
        if(ticket<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"买到了"+ticket--);
    }
}
  • 银行取钱,由于没有使用锁机制两个线程同时取钱导致了余额为负
package Thread;
public class Bank {
    public static void main(String[] args) {
        Account account =new Account(800,"彩礼");
        Drawing you=new Drawing(account,600,"你");
        Drawing girl=new Drawing(account,600,"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;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money=account.money-drawingMoney;
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.name+"余额为"+account.money);
        System.out.println(this.getName()+"手里的钱"+nowMoney);
    }
}
// 彩礼余额为-400
//彩礼余额为-400
//你手里的钱600
//girl手里的钱600
  • 由于我们可以通过private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:
synchronized方法(同步方法)
  • synchronized方法控制对“对象”的访问,每一个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,知道方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
  • 缺陷:若将一个大的方法申明为synchronized将会影响效率,方法里需要修改的内容才需要锁,锁的太多浪费资源
  • 使用synchronized方法修改上述买票例子
//只需在线程方法体实现的方法处加入修饰符 synchronized即可
private synshronized void bbuy() throws InterruptedException {
        if(ticket<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"买到了"+ticket--);
    }
}
synchronized(Obj){} (同步块)
  • Obj称之为同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this(有事我们需要同步的对象并不在执行方法所在的类当中,这是蛋蛋使用同步方法并不能达到效果),就是这个对象本身,或者是class
  • 同步监视器的执行过程
  • 第一个线程访问,锁定同步监视器,执行代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  • 使用synchronized(Obj){}修改上述银行取钱例子
//实现方法,Obj处设置需要被同步的对象,对对象的操作放入{}当中,整个过程在重写的方法中实现。
    @Override
    public void run() {
        synchronized (account){
            if (account.money-drawingMoney<=0){
                System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money=account.money-drawingMoney;
            nowMoney=nowMoney+drawingMoney;
            System.out.println(account.name+"余额为"+account.money);
            System.out.println(this.getName()+"手里的钱"+nowMoney);
        }

    }

死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的清醒,
  • 某一个同步块同时拥有两个以上对象的锁时,就可能发生死锁的问题
//由于在lucy拿到口红锁的同时,tom拿到了镜子的锁,所以程序无法继续执行
package Thread;
public class DeadLock {
    public static void main(String[] args) {
        Makeup test1=new Makeup(0,"lucy");
        Thread lucy=new Thread(test1);
        Makeup test2=new Makeup(1,"Tom");
        Thread tom=new Thread(test2);
        lucy.start();
        tom.start();
    }
}
class Lipstick{
}
class Mirror{
}
class Makeup implements Runnable{
    static Lipstick lipstick=new Lipstick();
    static Mirror mirror=new Mirror();
    int choice;
    String girlName;
    public 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(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);
            }
            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+"获得口红的锁");
                }
            }
    }

Lock(锁)

  • 从JDK5.0开始,Java提供了更强大的线程同步机制—通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util,concurrent.locks.Lock接口时控制多个相乘对共享资源进行访问的工具。锁提供了对共享资源的独立空间,每次只能有一个线程对Lock对象枷锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的时ReentrantLock,可以显示加锁,释放锁
//在与要修改对象的代码块外开启锁和关闭锁(关键字try{打开锁...方法体}finally{关闭锁})
class BuyTic implements Runnable{
    int ticket=10;
    private final ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticket--);
                }else {
                    break;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}
  • synchronized与Lock的对比
  • Lock是显示锁(手动打开和关闭,别忘记关闭),synchronized是隐式锁,出了作用域就自动释放
  • Lock只有代码块锁,synchronized有代码锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能较好,并且具有更好的扩展性(提供更多的子类)
  • 优先顺序:
Lock >同步代码块 (进入了方法体,分配了相应资源)>同步方法(在方法体之外)

练习

  • 线程不安全的集合
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(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(list.size());
    }
}
  • 修改:将对象list用synchronized(list){}锁上
new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }