Thinking in java学习笔记19:第二十一章(并发-中)

∥☆過路亽.° 提交于 2020-03-09 13:43:51

上一个笔记(并发-上)的内容:https://blog.csdn.net/asdfghj253/article/details/104050967

3.共享受限资源

 

3.1 不正确地访问资源

这节举了个例子来说明,我总结一下。那就是资源的原子性和可视性。

这里将资源设置了状态,并编写方法来提供资源是否可用的可观性,当状态为不可用,那么其他线程无法调用,等一线程结束状态改回可用,再给下一个线程使用。

 

3.2 解决共享资源竞争

上面那节主要是当一种思维来讲的,根据这个思维,这边讲了线程现在使用的一个资源机制,互斥和共享,通过加锁来解决并发模式的线程问题。这些都是采用序列化访问共享资源的方案:在给定时刻只允许一个任务访问共享资源。

而锁往往是锁一段代码,所以一段时间内只会有一个任务可以运行这段代码

3.2.1 synchronized 关键字 (同步)

java提供synchronized关键字(同步)来防止资源冲突,这个关键字将会检查锁是否可用,然后获取锁。执行代码,最后释放锁。

例: synchronized void test(){ ......}

这个test函数,一个时间段内只有一个任务可以调用。调用完释放锁,给下一个任务使用。

注意:使用synchronized的话,将里面的域(成员)设置成private是非常重要的,否则synchronized不能阻止其他函数来访问这个成员,会导致该域冲突。

3.2.2 使用显式Lock对象

相对于synchronized 关键字的使用,使用Lcok代码多了点,需在方法中自己写加锁,然后需要些try-finally 解锁。不过优点也在于此。它更加易于修改锁。不过一般来说synchronized 已经够用了

    private static Lock Mylock=new ReentrantLock();
    public void test(){
        Mylock.lock();
        try{
            /*......*/
        }finally {
            Mylock.unlock();
        }
    }

 

3.3 原子性与易变性

这一节讲了原子性和易变性,不过这两个性质我都是理解的,所以就不记笔记了。这里简单记录下里面使用的其它关键字

volatile关键字,这个关键字也和synchronized 关键字类似也是同步的意思,但是这个volatile属于轻量级的同步,它并不像synchronized一样完善

工作原理是保存数据到本地内存,再到主内存,再从主内存读取数据给其它线程

作用就是避免数据脏读。

PS:volatile限制太大,唯一安全的情况就是一个类中只有一个可变的域时使用。所以优先还是选择synchronized

 

3.4 原子类

Automic类(原子类):AtomicInteger、AtomicLong、AtomicReference

方法请自查,包括增删改。

 

3.5 临界区

临界区:防止多线程同时访问方法内的那部分代码段就是临界区,也是使用synchronized 关键字。

 

3.6 在其它对象上同步

正常使用的 synchronized void test(){ ......}  实质上用的是synchronized(this)

所以在其它对象上同步,也就是一个对象上的两个代码块一起同步,可以在两个代码块上使用synchronized(对象)的形式。

 

3.7 线程本地存储

书中解释得很棒:

线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。

PS:我的理解看来,实际上就是线程版的多态,初始态变量类型都一样,但之后多个线程就有多种变化形式,也就是说线程的数据不是公用的,而是每个线程都独立开来的。

创建和管理线程本地存储由java.lang.ThreadLocal类来实现。

ThreadLocal可以将static和单例变量转为线程私有,因为static和单例变量往往是线程公有的,而其它的变量比如public、private都是线程私有的。

 

4.终结任务

正常的终结方式之前已经用过了,比如yield(),这里也不记录了。

4.1 在阻塞时终结

这里先介绍线程的四种状态:新建、就绪、阻塞、死亡

1.新建:当线程被创建时的状态,它已被分配必要的系统资源并初始化,可由调度器转换为可运行状态或阻塞状态。

2.就绪:字面意思,准备就绪,随时都可以启动(调度器分配时间片(CPU时间)给线程,即可运行)

3.阻塞:线程能运行,但有条件阻止线程运行,这是调度器会忽略线程,不分配CPU时间,等阻塞重新变为就绪再恢复分配。

4.死亡:处于死亡或终止状态的线程不再可调度,不会得到CPU时间。任务死亡一般是从run()方法返回。

进入阻塞的原因

1.可能是调用了sleep()使任务进入了休眠状态

2.可能是调用了wait()使线程挂起,直到线程得到notify()或notifyAll()消息,重新唤醒,进入就绪状态

3.任务在等待某个输入/输出的完成

4.任务调用同步(加锁)方法,锁已有其他任务在使用

PS:这里特别说明了一个stop()方法,这个方法不释放线程获得的锁,所以不要用这里来停止线程。

 

4.2 中断

中断的方法有蛮多的,

1.首先是Thread类的interrupt()方法,它可以设置线程的中断状态,并抛出InterruptedException异常。

2.有了Executor后,它也带有中断方法,Executor调用shutdownNow()方法,这个方法可以发送一个interrupt()给Executor启动(executor())

的所有线程。

3.Executor使用submit()而不是executor()时,submit返回的是Future<?>类型,而这个泛型有个cancal()方法可以用来中断

被互斥而阻塞:

通过加锁来阻塞,synchronized关键字和加Lock锁

这里面Lock类中的ReentrantLock上阻塞的任务具备可以被断开的方法

    private static Lock Mylock=new ReentrantLock();
    //private Condition condition=Mylock.newCondition();
    public void test(){
        Mylock.lock();
        try{
            System.out.println("沙丁鱼不好吃,被臭醒!");
            Mylock.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            Mylock.unlock();
        }
    }

 

4.3 检查中断

这个检查线程是否中断,也可以看是否抛出InterruptedException异常来确定。

 

5.线程之间的协作

 

5.1 wait()和notifyAll()和notify()

wait()方法的英文都懂,就是等待的意思,这个wait()方法也是一样的,它表示线程的等待,

这里有两种形式的wait()

形式1:带有毫秒数的参数,和sleep()有些类似。不同的地方是wait(time)期间对象锁是释放的,然后可以通过notify()等方法提前唤醒。或到时间自动唤醒。

形式2:wait()不带任何参数,无限等待下去,直到notify()等方法唤醒。

PS:注意一点,wait()和notifyAll()等方法是放在同步控制块里面的,是通过控制线程来加锁(或释放锁)的。

notify()和notifyAll()都是唤醒线程,但差别在于notify只会从中选一个线程唤醒,而notifyAll将唤醒所有线程(当然这里的所有是针对等待这个对应的锁的所有线程)。

 

5.2 生产者和消费者

这里主要介绍一下使用显式的Lock和Condition对象来进行使用互斥和任务挂起。

public class LockMain {
    private static Lock Mylock=new ReentrantLock();
    private Condition condition=Mylock.newCondition();
    public void test(){
        Mylock.lock();
        try{
            System.out.println("沙丁鱼不好吃,被臭醒!");
            //condition.await();
            condition.signal();
            //condition.signalAll();
        }finally {
            Mylock.unlock();
        }
    }
    public void test1(){
        Mylock.lock();
        try{
            System.out.println("沙丁鱼在等待!");
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            Mylock.unlock();
        }
    }
    public static void main(String[] args){
         new LockMain().test1();
        new LockMain().test();
    }
}

唤醒方法也从notify()变成了signal()方法,然后wait()方法也变成了await()方法。

我设计的代码也很明显表现出等待的效果,线程处于等待状态,所以程序无法解锁,执行下一个方法。

 

5.3 队列

这里队列的意思,我举个例子来总结,流水线,分多道工序,比如:放入原材料->加工原材料->产品生产->包装产品

这四个步骤需要按顺序一个一个来,而且顺序不能乱,相当于四个线程,一个继承前一个线程的数据后再执行一样。

所以这里就引出了一个概念:队列。

这个队列java是有提供的,那就是java.util.concurrent.BlockingQueue接口。这个接口提供了大量队列类型的实现,一般可以使用LinkedBlockQueue这个无届队列。除此之外,也可以使用ArrayBlockingQueue队列,这个队列是有固定的尺寸的。

PS:这节队列有点搞不懂,以后再仔细学一下。

 

5.4 任务间使用管道进行输入输出

通过输入、输出在线程间进行通信,这里java的类库以“管道”的形式对线程间的输入/输出提供了支持。而这个类就是PipedWriter类和PipedReader类。用法和普通的输入输出有点差不多。

private PipedWriter out=new PipedWriter();

out.write(xxx);

private PipedReader in;
in=new PipedReader(out);

in.read();

 

6.死锁

死锁:顾名思义就是无法解开的锁。这里程序里是指任务之间互相等待任务完成,同时每个任务都在等待,无法继续线程,这样的任务等待循环叫死锁。

简单来说就是,鸡生蛋问题,是现有鸡还是先有蛋,鸡在等蛋出现,蛋在等鸡出现,导致卡死循环,都在等待对方的出现再自己无法出现,导致两者都不出现。

这时为解决这个死锁问题,我们就可以使用上面所讲得的wait(),notify()等方法,在造成卡死状态时,进行抛出,代码释放锁,破掉死循环。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!