多线程详解(三)——线程同步

一. 什么是线程同步

处理多线程问题时,当多个线程访问同一对象,并且某些线程还想修改这个对象时,就需要线程同步
线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。
线程同步的条件:等待队列 和 锁

如果不进行线程同步,处理多线程问题时会出现线程不安全的情况:

public class TestThread {
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test,"张三").start();
        new Thread(test,"李四").start();
        new Thread(test,"王五").start();
    }
}
class Test implements Runnable{
    private int ticketNum = 10;
    @Override
    public void run() {
        while (ticketNum > 0) {
            buy();
            try {
                Thread.sleep(100);  // 模拟延时,放大问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 买票方法
    public void buy(){  
        if (ticketNum<=0)
            return;
        System.out.println(Thread.currentThread().getName() + "抢到了第" + (ticketNum--) + "张票");
    }
}

结果:

李四抢到了第10张票
王五抢到了第8张票
张三抢到了第9张票
李四抢到了第7张票
张三抢到了第7张票
王五抢到了第6张票
李四抢到了第5张票
王五抢到了第4张票
张三抢到了第5张票
李四抢到了第3张票
张三抢到了第3张票
王五抢到了第3张票
张三抢到了第2张票
李四抢到了第1张票

二. 实现线程同步

由于同一进程的多个线程共享同一块存储空间,带来方便的同时也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他访问线程必须等待,使用后释放锁。

锁机制(synchronized)包括两种用法:synchronized方法synchronized块

① 同步方法(synchronized方法):

由于我们可以通过private关键字来保证数据对象只能通过方法访问,所以我们可以针对方法进行同步。

同步方法只需在方法前面加上synchronized关键字:public synchronized void method(){}

synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程堵塞,方法一旦执行,就独占该锁,直到方法执行完毕释放锁。

public class TestThread2 {
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test,"张三").start();
        new Thread(test,"李四").start();
        new Thread(test,"王五").start();
    }
}
class Test implements Runnable{
    private int ticketNum = 10;
    @Override
    public void run() {
        while (ticketNum > 0) {
            buy();
            try {
                Thread.sleep(100);  // 模拟延时,放大问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 买票方法
    public synchronized void buy(){  //加上synchronized关键字变为同步方法
        if (ticketNum<=0)
            return;
        System.out.println(Thread.currentThread().getName() + "抢到了第" + (ticketNum--) + "张票");
    }
}

结果:

张三抢到了第10张票
王五抢到了第9张票
李四抢到了第8张票
张三抢到了第7张票
王五抢到了第6张票
李四抢到了第5张票
王五抢到了第4张票
张三抢到了第3张票
李四抢到了第2张票
李四抢到了第1张票
② 同步块(synchronized块):

synchronized(Obj){}
Obj 称之为 同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器。
同步方法中无需指定同步监视器,因为其同步监视器就是This,即对象本身,或者是class。

模拟银行取钱,如果不进行同步则会出现线程不安全:

public class TestWithDraw {
    public static void main(String[] args) {
        Account account = new Account(200);
        Drawing first = new Drawing(account,150);
        Drawing second = new Drawing(account,100);
        first.start();
        second.start();
    }
}
class Account {
    int money; // 余额

    public Account(int money) {
        this.money = money;
    }
}
class Drawing extends Thread{
    private Account account;
    private int drawingMoney; // 要取的金额

    public Drawing(Account account, int drawingMoney) {
        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 -= drawingMoney;
        System.out.println("账户余额为:"+account.money);
    }
}

结果:

账户余额为:-50
账户余额为:-50

这里如果用同步方法进行线程同步:

public class TestWithDraw {
    public static void main(String[] args) {
        Account account = new Account(200);
        Drawing first = new Drawing(account,150);
        Drawing second = new Drawing(account,100);
        first.start();
        second.start();
    }
}
class Account {
    int money; // 余额

    public Account(int money) {
        this.money = money;
    }
}
class Drawing extends Thread{
    private Account account;
    private int drawingMoney; // 要取的金额

    public Drawing(Account account, int drawingMoney) {
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public synchronized 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 -= drawingMoney;
        System.out.println("账户余额为:"+account.money);
    }
}

结果还是不安全:

账户余额为:-50
账户余额为:-50

因为同步方法的同步监视器锁定的是this(当前对象本身),本例中锁定的是Drawing对象,而共享资源是Account对象,应锁定account,所以应采用同步块(synchronized块)进行线程同步:

public class TestWithDraw {
    public static void main(String[] args) {
        Account account = new Account(200);
        Drawing first = new Drawing(account,150);
        Drawing second = new Drawing(account,100);
        first.start();
        second.start();
    }
}
class Account {
    int money; // 余额

    public Account(int money) {
        this.money = money;
    }
}
class Drawing extends Thread{
    private Account account;
    private int drawingMoney; // 要取的金额

    public Drawing(Account account, int drawingMoney) {
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @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 -= drawingMoney;
            System.out.println("账户余额为:"+account.money);
        }
    }
}

正确结果:

账户余额为:50
Thread-1钱不够了,取不了

三. 死锁

多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一同步块同时拥有两个以上对象的锁就可能发生“死锁”的问题。

模拟死锁:

public class TestWithDraw {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"灰公主");
        Makeup g2 = new Makeup(1,"白雪公主");
        // g1 获得镜子等待口红,g2 获得口红等待镜子,两者相互等待导致死锁
        g1.start();
        g2.start();
    }
}
// 口红
class Lipstick{

}
// 镜子
class Mirror{

}
// 化妆
class Makeup extends Thread{
    // 口红、镜子资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    private int choice;
    private String name;

    public Makeup(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void makeup() throws InterruptedException {
        if (choice==0){
            synchronized (lipstick){  // 获得口红的锁
                System.out.println(this.name+"获得了口红");
                Thread.sleep(1000);
                synchronized (mirror){  // 获得镜子的锁
                    System.out.println(this.name+"获得了镜子");
                }
            }
        }else{
            synchronized (mirror){  // 获得镜子的锁
                System.out.println(this.name+"获得了镜子");
                Thread.sleep(1000);
                synchronized (lipstick){  // 获得口红的锁
                    System.out.println(this.name+"获得了口红");
                }
            }
        }
    }
}

结果:程序不会结束,一直在运行
在这里插入图片描述
产生死锁的四个必要条件:
① 互斥条件:一个资源每次只能被一个进程使用
② 请求与保持条件:一个进程因请求资源而堵塞时,对已获得的资源保持不释放
③ 不剥夺条件:进程获得的资源在未使用完之前,不能强行剥夺
④ 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

避免死锁:只要破坏发生死锁的一个或多个条件就能避免死锁发生

四. 使用Lock锁实现线程同步

从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。
Lock是一个接口,ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示地加锁、解锁。

public class Test {
    public static void main(String[] args) {
        TLock tLock = new TLock();

        new Thread(tLock).start();
        new Thread(tLock).start();
        new Thread(tLock).start();
    }
}

class TLock implements Runnable{
    private Lock lock = new ReentrantLock();  // 定义Lock锁
    private int ticketNum = 10;

    @Override
    public void run() {
        while (true) {
            lock.lock();  // 加锁
            try{
                if (ticketNum>0){
                    System.out.println(ticketNum--);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else
                    break;
            }finally {
                lock.unlock();  // 解锁
            }
        }
    }
}

结果:

10
9
8
7
6
5
4
3
2
1

参考资料:
Java并发编程:Lock https://www.cnblogs.com/dolphin0520/p/3923167.html.
Java并发编程:synchronized https://www.cnblogs.com/dolphin0520/p/3923737.html

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值