一文看完《Java多线程编程核心技术》这本书(上)

chapter1——线程基本知识

线程不同步:如果多个线程对同一个对象中的同一个实例变量进行操作,会出现值被更改、值不同步的情况,影响程序的执行流程。

关键字synchronized是在任意对象及方法上加锁,加锁的代码叫做"互斥区"或者临界区。这种方法叫做同步方法。

synchronized会让其他线程调用这个方法时,进行排队,调用前,判断有没有上锁,有就等待,没有就竞争争抢这把锁。

start和run的区别:start是main线程创造的线程Thread-0在运行run方法,run方法运行过程中Thread-0是当前线程,
而使用run是main线程的线程Thread-0运行run方法,但是当前线程还是main线程。

run 方法不会进行调度,就是顺序执行。而start是异步执行。

不管start还是run,使用构造方法构建一个线程一定是main线程运行。

在使用线程的时候Thread.currentThread()和"this和this.currentThread()"不一样,要注意。this和Thread.currentThread()说不定是主线程。

stop()方法对synchronized有破坏作用,不建议使用其中断。停止线程建议使用interrupt()方法。
interrupt对比stop比较软,就是为线程打了一个停止的标记,并不是真的停止线程,结合this.isInterrupt()方法停止线程。建议使用抛异常法。

yield放弃当前cpu一下,先让别人去抢自己放弃的,然后自己重新去抢。

chapter2——对象以及变量的并发访问

非线程安全其实会在多个线程对同一个对象中的实例变量(非static)进行并发访问时发生,后果就是脏读。

方法内部的私有变量(在方法内部声明的私有变量)不存在"非线程安全"的。

t03_synchronizedMethodLockObject1这个package的程序证明了synchronized锁住的是Object,而不是某一个方法。这意味着如果methodB是不带
synchronized关键字的,那么methodB是可以异步加载的,如果添加了那么methodB在获得Object的锁的对列上的。

可重入锁的概念就是:自己可以再次获取自己的内部锁,比如一个线程获得了某个对象的锁,此时这个锁还没有被释放,当其再次想要获取这个对象的锁的时候还是可以获取的。如果不可锁重入的话,就会造成死锁。具体理解参见t05_lockin。子类可以调用父类同步方法的锁。

线程占用锁的过程中出现异常立马释放锁。父类的同步方法,如果继承的子类不添加synchronized关键字,子类的这个方法是一个非同步方法。

用关键字synchronized声明方法在某种情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么b线程必须等待比较长事件,在这样的情况下可以使用synchronized同步语句块来解决。synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一对象进行加锁。

一个object中的同步代码块(this)被访问时,其他线程对同一个object中的其他同步代码块(this)将被阻塞。具体验证参考t11_doubleSynBlockOneTwo和t11_verity
和同步方法一样,synchronized(this)代码块也是锁定当前对象的。

多个线程调用同一个对象的不同名称的synchronized同步方法或synchronized(this)同步代码块,调用的效果是按顺序执行,也就是同步的,阻塞的。
这两个作用基本一样,这两个运行的时候,其他同步方法或者同步代码块调用呈阻塞状态。
同一时间只有一个线程可以执行这个运行的代码。

java还支持synchronized(非this对象),作用与同步方法和同步代码块(this)的第二个作用类似,同一时间只有一个线程会锁定这个非this对象去执行synchronized(非this对象)这里面的代码,并没有整体锁定这个object,和其他就算是同步方法也是没有竞争关系的,是异步的

synchronized(非this对象的x)有下面结论:(具体参见t14_synchronizedBlockLockAll)
多个线程同时执行synchronized(x)是同步效果;
当其他线程执行x对象中synchronized同步方法中呈同步效果;
当其他线程执行x对象中synchronized(this)代码块呈同步效果;
如果其他线程调用x的不叫synchronized方法时,是异步调用。

静态同步方法与synchronized(class)代码块:关键字synchronized应用到静态方法上,此时是对当前的Java文件的对应的Class类进行加锁。

String对象如果值相同,他们俩就是一个东西,内存地址都相同。这是String的常量池特性。此时会出现排队现象。

volatile的主要作用是使变量在多个线程间可见。

synchronized VS volatile

  1. volatile是线程同步的轻量级实现。volatile性能比synchronized好,synchronized能够修饰方法以及代码块,volatile只能修饰变量。JDK1.8以后synchronized
    的效率也变高了,所以还是多用synchronized。
  2. 多线程volatile不会阻塞,synchronized会阻塞
  3. synchronized保证原子性,间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。volatile能保证数据的可见性,
  4. volatile解决的是变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问资源的同步性。

线程安全主要就是原子性和可见性两个方面,java的同步机制都是围绕这两个方面来确保线程安全。

实例变量在内存运行情况

实例变量在内存之间工作的过程像下面这样:

1.read和load阶段:从主存复制变量到当前线程工作内存。
2.use和assign:执行代码,改变共享变量值
3.store和write阶段:用工作内存数据刷新主存对应变量的值。

对于volatile,java只保证从主内存(共享内存)加载到线程内存的是最新的,就是read是同步的,换句话是,volatile只保证线程读的这个变量是最新的。
但是load、use和assign是非原子性的,换句话说如果已经load了,在use的过程进行assign,改之后的值有没有被主内存同步过去保证不了

原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类型就是一个原子操作可用的类型,他可以在没有锁的情况下做到线程安全。
但是原子操作最好也用同步关键字

synchronized具有volatile的功能,而且还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

chapter3——线程间通信

线程间的通信是线程作为整体的必用方案之一。
线程可以进行通信,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会是程序员对各线程任务在处理的过程中进行有效的把控与监督。

wait和notify基本流程

对于一个Object,如果同步这个Object时,一个在wait的阻塞状态,wait会释放Object的锁。
notify可以唤醒一个因调用了wait 操作而处于阻塞状态的线程,使其进入就绪状态,被重新唤醒的线程需要重新获得那把,获得锁后会继续执行。

wait() notify()和notifyAll()

  1. wait方法可以是调用该方法的线程释放共享资源的锁,然后从运行状态退出进入等待队列,直到被再次唤醒。
  2. notify方法可以随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是说notify方法仅仅通知一个线程。
  3. notifyAll方法可以使所有正在等待队列中等待同一共享资源的全部线程从等待状态退出进入可运行状态。此时优先级最高的线程最先执行但也有可能是随机执行。这取决于JVM虚拟机的实现。

ps. 我测试的结果就是Mac的后等待的会先被唤醒起来运行。

在没有通信的情况下线程状态的改变

  1. 新创建一个新的线程对象后,在调用start方法,系统会为此线程分配CPU资源,使其处于Runnable可运行状态,这是一个准备运行的阶段。如果线程对象抢占到CPU资源,此线程就处于运行Running状态。
  2. 可运行状态和运行状态可相互切换,因为有可能线程运行一段时间后,有其他高优先级的线程抢占了CPU资源,此时线程就从运行状态变成了可运行状态。
    线程进入可运行状态主要分为下面五种情况:
    
    * 调用sleep()方法后经过的时间超过了指定的休眠时间
    * 线程调用的阻塞I/O已经返回,阻塞方法执行完毕
    * 线程成功地获得了试图同步的监视器
    * 线程正在等待某个通知,其他线程发出了通知
    * 处于挂起状态的线程调用了resume恢复方法。
    
  3. Blocked是阻塞的意思,例如遇到了IO操作,此时CPU处于空闲状态,可能会转而把CPU事件分配给其他线程,此时也可以称为暂停状态,blocked状态结束后进入可运行状态,等待系统重新分配资源
    出现阻塞的情况大体分为以下五种:
    
    * 线程调用sleep方法,主动放弃占用的处理器资源。
    * 线程调用的阻塞式IO方法,在方法返回前,该线程被阻塞。
    * 线程试图获得一个同步监视器,但来同步监视器正被其他线程所持有。
    * 线程等待某个通知。
    * 程序调用了suspend方法,将线程挂起。容易死锁,尽量不用。
    
  4. run方法运行结束后进入销毁阶段,整个线程执行完毕。
    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。
    就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。
    一个线程被唤醒后,才会进入就绪队列,等待CPU的调度,反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。

必须要理解就绪队列和阻塞队列

就绪队列就是处于可运行状态的线程队列。阻塞队列就是对于阻塞状态的线程队列
上面的各种状态说的一定要理解。下面这个是我的个人总结:

1.某个阻塞结束了,因为这个阻塞而被阻塞的会重新来到就绪队列。
2.就绪队列并不是就马上要运行了,还有可能没竞争到某一把锁重新回到阻塞队列,这是CPU根据队列前后和优先级重新调配的结果。
3.wait和notify是很确定的,wait的线程一定在阻塞队列,利用notify通知wait的队列后,不是一定开始运行,其会进入就绪队列,而且有可能又被阻塞回到阻塞队列。
4.sleep的时候线程进入阻塞队列,此时就绪队列的线程会被运行;sleep结束线程重新进入就绪队列。
5.suspend方法进入阻塞,resume会进入就绪。
6.没有获得锁一定被阻塞,获得锁会就绪,但不代表CPU立马就运行这个线程。

wait状态下的线程遇到interrupt会直接抛出InterruptedException异常
wait(long)带参数的就是,时间内被唤醒就唤醒,不唤醒时间到了自动唤醒。

条件判断用while

notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。
具体查看 t07_waitold。

if时,顺序执行,肯定要remove,此时多个remove会异常
while时,执行完继续判断条件为不为空,空的话继续等待不remove,不会有异常

管道进行线程间通信

chapter3之生产者与消费者

等待/通知模式最经典的案例就是“生产者/消费者”模式。有下面大概几种情况,不过原理都是基于wait/notify的。
P一般代表生产者Producer。C一般代表消费者Consumer。

一生产一消费:操作值

t01_value11:一个生产者一个消费者改变一个值。

多生产多消费:操作值 Notify

t02_valueManyMany:多个生产者多个消费者改变一个值,很容易造成假死现象。

假死过程

因为生产者唤醒的不一定下一个就是消费者,如果还是生产者,这个生产者阻塞了以后,下个生产者直接阻塞不在继续唤醒。此时两个生产者已经阻塞,如果唤醒的一个消费者消费完唤醒的不是生产者而还是消费者,第一个唤醒后阻塞了以后,第二个消费者被唤醒由于被消费过了直接wait阻塞,于是4个都被阻塞。假死现象产生。

多生产多消费:操作值 NotifyAll

t02_valueManyManyNotifyAll:使用notifyAll的方法去进行多个生产者多个消费者改变一个值。

不再假死的原因

生产者生产完唤醒所有线程,所有线程都是就绪,如果后面还是生产者,消费者此时被阻塞是因为没有竞争到锁,所以两个生产者都wait时候,消费者可以竞争锁,而且也被唤醒了,所以重新竞争到锁的那个消费者被运行,去消费,消费后如果又是消费,消费者阻塞后,释放锁,生产可以拿到锁,其中一个有竞争到锁,生产。

1生产1消费:操作栈

t03_stack11:使用堆栈,生产者往堆栈添加数据,消费者从堆栈取出数据。数据的size()不大于1

1生产多消费:操作栈

t03_stack1many:使用堆栈,生产者往堆栈添加数据,多个消费者从堆栈取出数据。数据的size()不大于1。
用if条件判断会报异常,注意应该用while。
while的好处再说一遍:就是wait之后被唤醒,while仍然会判断条件,而if的话不判断直接运行。
虽然while,但此时会出现消费者一直通知消费者的情况还是可能假死,所以可以都改成notifyAll,但是还可以用while做条件判断。

对于这个例子,一定从两个角度看,第一个是if和while,第二个是notify和notifyAll。

2*2搭配:
if+notify:因为有多个消费者,消费者之间会进行唤醒。所以当消费者连续进行唤醒会出现异常。最终,都会进入假死现象。
if+notifyAll:会造成所有的线程最后只有一个会运行成功,其他的都会因为异常死掉,最后就会变成一个生产者和一个消费者的问题还是可以顺利运行的。
while+notify:的结果就是每次被唤醒后仍然会进行条件判断,那结果就不会出现if那样异常。但是还是有可能出现假死的现象。
while+notifyAll:此时才是真正的一个生产多个消费。一个生产者负责生产,多个消费的同时进行消费。

多生产1消费:操作栈

t03_stackmany1:使用堆栈,多个生产者往堆栈添加数据,1个消费者从堆栈取出数据。while+notifyAll

多生产多消费:操作栈

t03_stackManyMany:使用堆栈,多个生产者往堆栈添加数据,多个消费者从堆栈取出数据。while+notifyAll

Join

join()的作用就是等待线程对象销毁。
如果在z线程的方法里执行了x.join();方法。join使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待x销毁后继续执行z的代码。
join的过程中碰到interrupt方法,等待的线程直接会异常,但是被join的线程仍然继续运行。
join(long)是设定等待的时间。注意join内部是用wait实现的,所以x.join(3000)这三秒钟,x的锁是释放的,等待三秒后无论x什么情况z都继续运行。

注意一个问题x.join(3000),join是需要先争到x的锁,然后内部才在wait部分释放的。

ThreadLocal类

ThreadLocal类可以实现每一个线程都有自己的共享变量,让每个线程都可以绑定自己的值,不同线程的值是可以放在ThreadLocal类里进行保存的。
可以将ThreadLocal比喻成全局存放的盒子,盒子中可以存储每个线程的私有数据。

InheritableThreadLocal

InheritableThreadLocal类可以在子线程取得父线程继承下来的值。

发表评论

电子邮件地址不会被公开。