Java多线程05:线程同步

线程同步机制

多个线程访问同一个对象,为了保证数据在方法中被访问时的正确性,就需要线程同步

线程同步就是一种等待机制,多个线程进入这个对象的等待池,形成队列,等待前面的线程执行完毕,下一个线程再使用

在访问时加入锁机制(synchronized),当一个线程获得对象的排他锁就能独占资源,其他线程必须等待,使用后释放锁即可

形成条件:队列+锁

线程不安全案例

银行取钱

public class Main {

    public static void main(String[] args) {

        Account account = new Account(110);

        Drawing me = new Drawing(account, 50, "我");
        Drawing you = new Drawing(account, 100, "你");

        new Thread(me, "我").start();
        new Thread(you, "你").start();
    }
}

/**
 * 账户类
 */
class Account {

    int totalMoney;

    public Account(int totalMoney){
        this.totalMoney = totalMoney;
    }
}

/**
 * 取钱类,需要账户名、取钱数、取钱人
 */
class Drawing implements Runnable {

    Account account;
    int needMoney;
    String name;

    public Drawing(Account account, int needMoney, String name){

        this.account = account;
        this.needMoney = needMoney;
        this.name = name;
    }

    @Override
    public void run() {

        if (account.totalMoney - needMoney < 0){
            System.out.println(Thread.currentThread().getName() + ",余额不足");
        }
        else{

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            account.totalMoney -= needMoney;
            System.out.println(Thread.currentThread().getName() + "取走了" + needMoney);
            System.out.println("余额还有" + account.totalMoney);
        }
    }
}

ArrayList集合

import java.util.ArrayList;

public class Main {

    public static void main(String[] args)  {

        ArrayList<String> li = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {

            new Thread(()->{
                li.add(Thread.currentThread().getName());
            }).start();
        }
        
        /**
         * 打印的列表长度没有10000,因为线程同时对一个位置进行操作,覆盖了其他元素
         */
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(li.size());
    }
}

线程同步之Synchronized

类似于private关键字保证数据对象只能被方法访问,使用synchronized关键字来实现同步

包括两种用法:synchronized方法和synchronized(obj)块

synchronized方法

该方法控制对“对象“的访问,每个对象对应一把锁,每个方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞

该方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺点:将一个大的方法申明为synchronized会影响效率,因为只有需要修改的内容才需要锁,只读的话不应该被锁,此时只能用synchronized(obj)块

public class Main implements Runnable {

    int num = 10;

    /**
     * synchronized同步方法默认锁的对象是this本身
     * 如果有多个对象就得用synchronized块指定共同访问的对象
     */
    @Override
    public synchronized void run() {

        while (true) {

            if (num <= 0) {
                break;
            }

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "抢到了第" + num-- + "张票");
        }
    }

    public static void main(String[] args) {

        Main main = new Main();

        new Thread(main, "老师").start();
        new Thread(main, "学生").start();
        new Thread(main, "黄牛").start();
    }
}

synchronized(obj)块

  • obj称为同步监视器,推荐使用共享资源的对象
  • 同步方法的监视器就是this对象本身,但是有多个对象操作时,就需要指定一个共有对象,否则每个对象都以自己作为监视器,就失去了同步的意义
public class Main {

    public static void main(String[] args) {

        Account account = new Account(110);
        Drawing me = new Drawing(account, 50, "我");
        Drawing you = new Drawing(account, 100, "你");

        new Thread(me, "我").start();
        new Thread(you, "你").start();
    }
}

class Account{

    int totalMoney;

    public Account(int totalMoney){
        this.totalMoney = totalMoney;
    }
}

class Drawing implements Runnable{

    Account account;
    int needMoney;
    String name;

    public Drawing(Account account, int needMoney, String name){

        this.account = account;
        this.needMoney = needMoney;
        this.name = name;
    }

    @Override
    public void run() {

        /**
         * 不同对象共同操作的是account对象,因此要锁的是account
         */
        synchronized (account) {

            if (account.totalMoney - needMoney < 0) {
                System.out.println(Thread.currentThread().getName() + ",余额不足");
            } else {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                account.totalMoney -= needMoney;
                
                System.out.println(Thread.currentThread().getName() + "取走了" + needMoney);
                System.out.println("余额还有" + account.totalMoney);
            }
        }
    }
}

存在的问题:

一个线程持有锁会导致其他所有需要此锁的线程挂起

加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题

低优先级的线程持有锁会导致高优先级的线程一直等待,造成优先级倒置,引起性能问题

若将一个大的方法申明为synchronized方法会影响效率

线程同步之Lock

Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

ReentrantLock类(可重入锁)实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,可以显式加锁、释放锁

import java.util.concurrent.locks.ReentrantLock;

public class Main {

    public static void main(String[] args) {

        Account account = new Account(100);
        Drawing drawing = new Drawing(account, "小红", 50);
        Drawing drawing1 = new Drawing(account, "小明", 80);

        new Thread(drawing).start();
        new Thread(drawing1).start();
    }
}

class Account{

    int totalMoney;

    public Account(int totalMoney){

        this.totalMoney = totalMoney;
    }
}

class Drawing implements Runnable {

    Account account;
    String name;
    int needMoney;

    public Drawing(Account account, String name, int needMoney) {

        this.account = account;
        this.name = name;
        this.needMoney = needMoney;
    }

    /**
     * 创建ReentrantLock对象lock
     * 为了确保多个对象调用时是同一个lock,使用static关键字修饰
     */
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        /**
         * lock()方法加锁,和try...finally语句一起使用
         */
        lock.lock();

        try {

            if (account.totalMoney - needMoney <= 0) {
                System.out.println(name + "取钱,余额不足");
            }
            else {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                account.totalMoney -= needMoney;
                System.out.println(name + "取走了" + needMoney + ",还剩" + account.totalMoney);
            }
        }

        /**
         * 在finally语句中放unlock()方法释放锁
         */
        finally {
            lock.unlock();
        }
    }
}

synchronized和Lock对比:

Lock是显式锁,需要手动释放;只有代码块锁没有方法锁

Lock锁可以让JVM花费较少的时间来调度线程,性能更好,且具有更好的拓展性(有很多子类)

优先使用顺序:Lock > 同步代码块 > 同步方法

死锁

多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,导致都停止执行的情形

某个同步块同时拥有两个以上对象的锁时,就可能发生”死锁“的问题

产生死锁的四个必要条件:(破除任何一个条件就可以避免死锁发生)

互斥条件:一个资源每次只能被一个进程使用

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:进程已获得的资源,在使用完之前,不能强行剥夺

循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

public class Main {

    public static void main(String[] args) {

        MakeUp makeUp1 = new MakeUp("小红", 0);
        MakeUp makeUp2 = new MakeUp("小花", 1);

        new Thread(makeUp1).start();
        new Thread(makeUp2).start();
    }
}

class Mirror{}

class Lip{}

class MakeUp implements Runnable{

    String name;
    int choice;

    public MakeUp(String name, int choice){

        this.name = name;
        this.choice = choice;
    }

    /**
     * 将镜子和口红属性设置为static,这样可以保证所有对象调用的是同一个镜子和口红
     */
    static Mirror mirror = new Mirror();
    static Lip lip = new Lip();

    @Override
    public void run() {

        if (choice == 0) {

            synchronized (mirror) {

                System.out.println(name + "抢到了镜子");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                /**
                 * 在同步块里嵌套一个同步块才会死锁,不嵌套就不会死锁
                 */
                synchronized (lip) {
                    System.out.println(name + "抢到了口红");
                }
            }
        } else {

            synchronized (lip) {

                System.out.println(name + "抢到了口红");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (mirror) {
                    System.out.println(name + "抢到了镜子");
                }
            }
        }
    }
}
posted @ 2021-09-16 17:41  振袖秋枫问红叶  阅读(44)  评论(0)    收藏  举报