Java多线程系列(二):并发同步

线程安全问题

在多线程编程中,线程安全是一个最为关键的问题,单线程不会出现线程安全问题,但是在多线程中,有可能会出现同时访问同一个共享,可变资源的情况,这种资源可以是,一个变量,一个对象,一个文件等,我们称这种资源为临界资源,特别注意以下俩点
(1)共享:意味着资源可以有多个线程同时访问
(2)可变:意味着该资源可以在生命周期内被修改
所以当多个线程同时访问这个资源时,就会存在一个问题:由于每个线程执行过程是不可控的,会导致共享数据的错乱,所以需要采用同步机制来协同对象可变状态的访问,不过多个线程执行同一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量在每个线程私有的栈中,因此不具有共享性,不会导致线程安全问题

如何解决线程安全问题
实际上所有并发模式,在解决线程安全问题,都是采用同步互斥访问,即同一时刻只能有一个线程访问临界资源,换句话说就是,在临界资源前加一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

在Java中提供两种方式来实现同步互斥访问,synchronized 和 Lock,下面我们简单了解一下这俩个

synchronized

Java多线程中的同步机制会对资源进行加锁,保证在同一时间只有一个线程可以操作对应资源,避免多程同时访问相同资源发生冲突。Synchronized是Java中的关键字,它是一种同步锁,可以实现同步机制。

synchronized 方法

修饰普通方法 一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。

修饰静态方法 由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。

synchronized 代码块

修饰代码块 其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。

范围
Synchronized方法控制范围较大, 它会同步对象中所有Synchronized方法的代码。 Synchronized代码块控制范围较小, 它只会同步代码块中的代码, 而位于代码块之外的代码是可以被多个线程访问的。 简单来说 就是 Synchronized代码块更加灵活精确。

可重入性
一般,一个线程请求由其他线程持有的锁,发出请求的线程会阻塞,然而java内置锁是可重入的,因此如果某个线程试图获得一个已经由他自己持有的锁,这就会成功,可重入锁最大的作用是避免死锁

死锁

public class MyClass1 {

    public synchronized void show1(){
        Log.d("mmm","is show1");
    }
}
public class MyClass2 {

    public synchronized void show2(){
        Log.d("mmm","is show2");
    }
}

public class MyThread11 extends Thread {

    private final MyClass1 C1;
    private final MyClass2 C2;

    public MyThread11(MyClass1 myClass1, MyClass2 myClass2) {
        this.C1 = myClass1;
        this.C2 = myClass2;
    }

    @Override
    public void run() {
        synchronized (C1){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //醒来后
            Log.d("mmm","线程1醒来,准备访问C2.show2");
            C2.show2();
        }
    }
}
public class MyThread22 extends Thread {
    private final MyClass1 C1;
    private final MyClass2 C2;

    public MyThread22(MyClass1 myClass1, MyClass2 myClass2) {
        this.C1 = myClass1;
        this.C2 = myClass2;
    }

    @Override
    public void run() {
        synchronized (C2) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //醒来后
            Log.d("mmm", "线程2醒来,准备访问C1.show1");
            C1.show1();
        }
    }
}

 MyClass2 myClass2 = new MyClass2();
        MyClass1 myClass1 = new MyClass1();

        MyThread11 myThread11 = new MyThread11(myClass1, myClass2);
        MyThread22 myThread22 = new MyThread22(myClass1, myClass2);

        myThread11.start();
        myThread22.start();

看下log

08-01 06:17:02.365 2500-2867/com.example.jh.rxhapp D/mmm: 线程2醒来,准备访问C1.show1
08-01 06:17:02.365 2500-2866/com.example.jh.rxhapp D/mmm: 线程1醒来,准备访问C2.show2

我们分析一下死锁流程

t1.start();
t1锁住c1对象
t1休息3秒
t2.start();
t2锁住c2对象
t2休息3秒
t1醒来,调用c2的show2方法
	此时发现c2被锁定,所以t1对列等待
t2醒来,调用c1的show1方法
	此时发现c1被锁定,所以t2对列等待

不同的线程都在等待,不可能被释放的锁,导致所有的任务都无法完成,这就是死锁。

总结
用一句话来说,synchronized 内置锁是一种对象锁(锁的是对象而不是引用),作用粒度是对象,可以用来实现临界资源的同步互斥访问,是可重入的,对于临界资源有:
(1)若资源是静态的,即被staic关键字修饰的,那么访问他的方法必须都是同步的且静态的,synchronized 块必须是class锁
(2)若资源是非静态的,即没有被staic关键字修饰的,那么访问他的方法必须是同步的,synchronized 块是实例对象锁

synchronized 主要包含俩个特征
(1)互斥性:保证同一个时刻,只有一个线程可以执行一个方法或者一个代码块
(2)可见性:保证线程工作内存中变量与公共内存中的变量同步,使多线程读取公共变量时可以获得最新的值使用

如果一个代码块被synchronized 修饰了,当一个线程获取了对应的锁,并执行改代码块,其他线程便只能一直等待直至;占有锁的线程释放锁,事实上,占有锁的释放锁有下面几种情况
(1)代码块执行完毕
(2)发生异常
(3)在该线程中调用wait

Lock

既然有了synchronize 为什么还要有Lock,考虑一下三种情况
(1)在使用synchronize关键字的情况下,假如占有锁的线程有io或者其他原因被阻塞了(比如调用sleep),但是有没有释放锁,就只能一直等待,别无他法,这样会极大影响执行效率,因此就需要一种机制,可以不让等待的线程一直无限期的等待下去,比如只等待一段时间(解决方案:tryLock(long time,TimeUnit unit))或者能够响应中断(解决方案:lockInterruptibly()),这种可以通过lock来解决
(2)我们知道当多个线程,发生读写操作时,读写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,这样就会导致一个问题,当多个线程只进行读操作时,也只有一个线程可以进行读操作,其他线程只能等待锁的释放无法进行读操作,因此需要一种机制,当多个线程都是读操作时,线程间不会发生冲突,同样Lock也能解决这种问题(解决方案:ReentrantReadWriteLock)
(3)我们可以通过Lock得知有没有成功获取到锁(解决方案:ReentrantLock),这个是synchronize无法办到的

Lock比synchronize关键字更灵活,更广泛,粒度更细,提供了更多的功能。但是要注意下面几点:
(1)synchronize是java的关键字,因此是java的内置特性,是基于JVM实现的,经编译过后会在同步代码块前后分别形成monitorenter 和 monitorexit俩个字节码指令,而Lock是一个java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问
(2)采用synehronize关键字不需要用户手动去释放锁,当synchronize代码块或方法执行完毕后,会制动释放锁,而Lock需要手动释放锁(发生异常是不会自动释放锁),如果没有主动释放锁,就有可能导致死锁问题。

通过查看Lock的源码可知,Lock 是一个接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;  // 可以响应中断
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  // 可以响应中断
    void unlock();
    Condition newCondition();
}

下面来逐个分析Lock接口中每个方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作

lock()
 在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?首先,lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

ReentrantLock lock = new ReentrantLock();
        //获取锁
        lock.lock();
        try {
            //处理任务
        } catch (Exception e) {

        } finally {
            //释放锁
            lock.unlock();
        }

tryLock() & tryLock(long time, TimeUnit unit)
 tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

一般情况下,通过tryLock来获取锁时是这样使用的:

 ReentrantLock lock = new ReentrantLock();
        if (lock.tryLock()){
            try {
                //做一些处理
            }catch (Exception e){

            }finally {
                lock.unlock();//释放锁
            }
        }else {
            //如果获取不到锁怎么办
        }

lockInterruptibly()
lockInterruptibly方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,这个线程能够响应中断,及中断等待状态,例如俩个线程,同时通过lockInterruptibly去获取锁,假如A线程获取到了锁,而B线程在等待,那么对线程B调用threadB.interrupt()方法,可以中断线程B的等待状态
由于lockInterruptibly方法抛出了异常,该方法必须放在try代码块中或者把异常抛出去,推荐使用后者,因为,如果将获取锁放在try语句中,则必定会执行finally语句的解锁操作,如果线程在获取锁时被中断则在执行解锁操作会发生异常,因为该线程未得到锁。

当一个线程获取了锁之后,是不会被interrupt中断的,因为interrupt方法只能中断阻塞过程中的线程,而不能中断正在运行中的线程,与synchronize相比,如果一个线程处于等待锁的状态,只能一直等待下去

ReentrantLock
ReentrantLock是Lock的唯一实现类,我们用ReentrantLock来创建lock的实例

Lock的正确使用

在使用Lock的时候无论哪种方式获取锁,习惯上最后好一律放在try。。catch的上方,因为我们一般吧unlock的方法放在finally块中,如果线程没有获取到锁,在执行finally的解锁,或发生异常,因为该线程未获取锁,却执行了解锁操作

public class Test {
//1 这个必须是成员变量,如果是局部变量的话,每个线程都会保存一个副本,那么每个线程的得到的lock.lock(),是不同的锁,达不到同步互斥的目的
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test,"A");
        MyThread thread2 = new MyThread(test,"B");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  

    public void insert(Thread thread) throws InterruptedException{
        //2 注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将 InterruptedException 抛出,原因见上面
        lock.lockInterruptibly(); 
        try {  
            System.out.println("线程 " + thread.getName()+"得到了锁...");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {              // 耗时操作
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"执行finally...");
            lock.unlock();
            System.out.println("线程 " + thread.getName()+"释放了锁");
        } 
        System.out.println("over");
    }
}

class MyThread extends Thread {
    private Test test = null;

    public MyThread(Test test,String name) {
        super(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println("线程 " + Thread.currentThread().getName() + "被中断...");
        }
    }
}/* Output: 
        线程 A得到了锁...
        线程 B被中断...
 *///:~

ReadWriteLock
 ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个来获取写锁,也就是说将对临界资源的读写操作,分为俩个锁分配给线程,从而使得多个线程可以同时进行读的操作

Lock和synchronize选择

总的来说他们俩个有一下几个不同点
(1)Lock是一个接口,是JDK层面的实现,synchronize是java中的关键字,是java的内置特性,是JVM层面的实现
(2)synchronize在发生异常时,会自动释放锁,不会造成死锁,而Lock在发生异常时,如果没有主动的unlock释放锁,就很可能造成死锁问题,因此要在finally块中释放锁
(3)Lock可以让等待锁的线程响应中断,而是用synchronize,会一直等待下去,不能响应中断
(4)通过Lock可以知道是否成功获取锁,而synchronize不行
(5)Lock可以提高多个线程读操作的效率
在性能上如果资源竞争不激烈,是差不多的,而当资源竞争激烈的时候,Lock的性能大大超过synchronize,所以具体来说要根据情况选择

锁的一些概念介绍

可重入锁
如果具备可重入性,则称为可重入锁,Lock和synchronize都是可重入锁,可重入性表明了锁的分配粒度,基于线程的分配,而不是基于方法调用的分配,举个既简单的例子,一个线程执行一个synchronize方法method1 而method1 会调用synchronize方法method2 ,这时,不用重复申请锁,而可以直接执行method2,由于线程已经持有该对象的锁,就不用重复申请,这就是可重入性

可中断锁
可中断锁就是可以响应中断的锁,在java中synchronize就不是可中断锁,而Lock是可中断锁
如果一个线程A正在执行锁中的代码,而线程B在等待获取该锁,由于等待的时间过长,线程B不想再等待,想先处理其他事情,我们可以让他中断自己活着在别的线程中断他,这就是可中断锁,在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。

公平锁
公平锁尽量以请求锁的顺序来获取锁,比如同时有多个线程在等待锁,等待时间最久的(最先请求的)优先获得,这就是公平锁,而非公平锁,就是不按照请求顺序,这就有可能导一些线程,一直获取不到锁

在java中synchronize是非公平锁,不能保证顺序,而Lock相关的,默认情况下是非公平锁,但是可以设置为公平锁

读写锁
读写锁就是将临界资源分为了两个锁,一个读锁,一个写锁,正式因为有了读写锁,才使得多个线程之间的读操作不会发生冲突,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

生产者和消费者问题

//包子铺
public class BaoZiPu {
    ArrayList<String> list = new ArrayList<>();


    public synchronized void setBaoZi(String baoZi) {
        list.add(baoZi);
        //唤醒所有线程
        notifyAll();
    }

    public synchronized String getBaoZi() {
        if (list.size() <= 0) {//如果没有包子
            try {
                //让当前线程等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取一个包子
        String s = list.get(0);
        //移除一个包子
        list.remove(0);
        return s;
    }
}
//消费者
public class GetThread extends Thread {
    private final BaoZiPu mBaoziPu;

    public GetThread(BaoZiPu baoZiPu) {
        this.mBaoziPu = baoZiPu;
    }

    @Override
    public void run() {
        while (true){
            String baoZi = mBaoziPu.getBaoZi();
            Log.d("mmm", "我得到了一个" + baoZi);
        }
    }
}
//生产者
public class SetThread extends Thread {
    private final BaoZiPu mBaoZiPu;

    public SetThread(BaoZiPu baoZiPu) {
        this.mBaoZiPu = baoZiPu;
    }

    @Override
    public void run() {
        while (true) {
            try {
                //每3秒制造一个包子
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mBaoZiPu.setBaoZi("包子");
        }
    }
}

BaoZiPu baoZiPu = new BaoZiPu();

GetThread getThread = new GetThread(baoZiPu);
SetThread setThread = new SetThread(baoZiPu);

getThread.start();
setThread.start();

1.共享资源:包子铺
2.生产线程:SetThread–>添加包子
3.消费线程:GetThread–>买包子

由于两个线程无序访问,所以,很可能"消费者"会得到一个null值,这不是我们想看到的。
我们希望做到的:不论是否有包子,消费者都能得到一个包子;

1.在包子铺:获取包子的方法:getBaozi(),判断,如果没有包子了,让消费者"等待(Object–>wait())"
设置包子的方法:setBaozi(),没设置一个包子,都会"唤醒(Object–>notifyAll()/notify())“所有等待的线程;
2.注意:
1).此例只使用与"单生产"与"单消费”,不适用"多生产"和"多消费";
2).wait()方法和notify()/notifyAll()方法的调用,一定要在"同步方法/代码块"内,否则会抛异常;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值