多线程学习(二)


前言

  上一期分享了一些关于多线程的基础的知识,然后有提到线程安全问题,是由于没有提供数据访问保护,导致多个线程先后修改共享数据,而没有同步,导致读到的数据是“脏”数据。既然是因为数据没有同步造成的,那么我们就聊一聊如何实现线程同步。


一、什么是线程同步?

  线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

二、线程同步的4种方法

1.synchronized 关键字

  先来介绍一下这个synchronized关键字。它是一个java中的关键字,它可以用来修饰方法/代码块,它的作用就是可以保证同一时刻最多只有一个线程执行被synchronized修饰的方法/代码块。属于悲观锁。它实现同步的方式也很简单,只需要将可能会发生线程不安全的代码用synchronized关键字包起来就好。
还是售票系统这个例子,代码如下:

public class Thread06 {
    public static void main(String[] args) {
        Count1 count = new Count1();
        Thread t1 = new Thread(count, "线程1======");
        Thread t2 = new Thread(count, "线程2");
        t1.start();
        t2.start();
    }
}

class Count1 implements Runnable {
    private int count = 100;

    @Override
    public void run() {

        while (true) {
            synchronized (this) {
                if (count == 0) {
                    break;
                }
                try {
                    //增加出现线程不安全情况的几率,为了演示方便
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName() + "(" + count + ")");
            }
        }
    }
}

通过synchronized来同步count–这个操作,保证当前线程操作完了,别的线程才能进行操作。运行结果如下:
在这里插入图片描述

2.wait()、notify()和notifyAll()方法

方法名方法描述
wait()让当前线程进入等待状态。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
notify()唤醒在该对象上等待的某个线程
notifyAll()唤醒在该对象上等待的所有线程

  synchronized锁是悲观锁,被它修饰的代码同时只能被一个线程执行,假如A线程在只能synchronized修饰的代码,B线程想执行这段代码只能等到A线程执行完之后才可以执行。那么有没有办法让线程A先“停下来”,让B线程去执行呢?
  办法是有的,可以通过上述表格列出的Object类中的几个方法来实现。实现过程可以让A线程调用wait()方法,释放掉锁,让B线程获取所资源执行完后,可以使用notify()或notifyAll()方法来唤醒等待中的A线程,再让A线程去执行。
  之前线程的执行顺序是随机的,那么现在我们就可以通过这三个方法来控制他们的执行顺序了,话不多说,一道经典的面试题:两个线程,一个打印5次A,另一个打印5次B,现在要让AB交替打印,如何实现(这里使用这三个方法实现):
实现代码如下

public class PrintAB {

    // 该变量可以理解成:上一次打印是否是打印的字符 A。
    private volatile boolean flag = false;

    /**
     * 打印字符 A 的方法
     */
    private synchronized void printA(){
        try {
            // 判断上一次打印是否是打印的 A,如果是就进行等待,如果不是就执行下面的代码。
            while (flag){
                wait();
            }
            System.out.println("A");
            flag = true;
            // 唤醒在等待的线程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 打印字符 B 的方法
     */
    private synchronized void printB(){
        try{
            // 判断上一次打印是否是打印的 B,如果是就进行等待,如果不是就执行下面的代码。
            // 注意这里是去反,因为上次打印如果不是A,肯定就是B。
            while (!flag){
                wait();
            }
            System.out.println("B");
            flag = false;
            // 唤醒在等待的线程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
	public static void main(String[] args) {
		
		PrintAB pAB = new PrintAB();
		for(int i = 0; i < 10; i++) {
			//打印A
			new Thread(pAB::printA).start();
			//打印B
			new Thread(pAB::printB).start();
		}
	}

}

运行结果如下:
在这里插入图片描述

3.Lock锁

  Java5新增加了Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步。下面是它的一些常用api

方法名方法描述
lock()获取锁,调用该方法当前线程会获取锁,当锁获取后,从该方法返回。
unlock()释放锁
tryLock()尝试非阻塞地获取锁,调用该方法后立即返回,如果能够获取锁则返回true,否则返回false。
tryLock(long timeout,TimeUnit unit)如果获取到锁,立即返回true,否则会等待参数给定的时间单元,在等待的过程中,如果获取到了锁,就返回true,如果等待超时则返回false。
lockInterruptibly()可中断地获取锁,与lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程。
newCondition()获取等待通知组件,在组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将会释放锁。

通过Lock锁也可以实现交替打印AB,代码演示如下:

public class PrintAB2 {
    private Lock lock = new ReentrantLock();
    // 创建三个对象  要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();

    private volatile boolean flag = false;

    public void printA() {
        lock.lock();
        try {
            if (flag) {
                try {
                    conditionA.await();//A等待   造成当前线程在接到信号或被中断之前一直处于等待状态。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            System.out.println("A");
            conditionB.signal();//唤醒B   唤醒一个等待线程。
            flag = true;//重新赋值
        } finally {
            lock.unlock();//A解锁 把资源给其他人使用
        }
    }

    public void printB() {
        lock.lock();
        try {
            if (!flag) {
                try {
                    conditionB.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            System.out.println("B");
            conditionA.signal();
            flag = false;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintAB2 printAB2 = new PrintAB2();
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    printAB2.printA();
                }
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    printAB2.printB();
                }
            }
        }).start();
    }
}

运行结果如下:
在这里插入图片描述


总结

下期分享死锁以及经典的生产者消费者问题。记得点赞关注哟!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值