学习笔记之多线程笔记
Posted 可持续化发展
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记之多线程笔记相关的知识,希望对你有一定的参考价值。
目录
多线程
1、synchronized
关键字synchronized 使用场景:当多个线程对同一个对象的同一个实例变量进行操作时,为了避免非线程安全问题,就用synchronized。synchronized的主要作用:保证同一时刻,只有一个线程可以执行某一个方法或代码块。synchronized 可以修饰方法和代码块。
synchronized 三个特征:可见性、原子性、禁止代码重排序。synchronized可以用于解决脏读、多线程死锁等问题。
非线程安全问题:当多个线程对同一个对象中的同一个实例变量进行并发访问时,就可能会产生非线程安全问题。产生的后果就是:脏读,即读到的数据其实是被更改过的。脏读是不同线程“争抢”实例变量的结果。
线程安全是指获得的实例变量的值是经过同步处理的,不会有脏读现象。
方法内的变量是线程安全的。非线程安全问题只存在于实例变量中。
解决方法:在修改实例变量的方法前加关键字synchronized。有个结论是:两个线程同时访问同一个对象的同步方法时一定是线程安全的。不管哪个线程先运行,这个线程进入synchronized声明的方法时就会持有该方法所属对象的锁。方法执行完后自动解锁。之后下一个线程才会进入同步方法里。不解锁,其他线程执行不了用synchronized声明的方法。
+++++++++++++++++++++
同步synchronized在字节码指令中的原理
在方法中使用synchronized实现同步的原因是:使用了flag标记ACC_SYNCHRONIZED,当调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。
我们在cmd中用javap来反汇编代码,进行分析。如果使用synchronized代码块,则使用monitorenter和monitorexit指令进行同步处理。
同步:按顺序执行A和B两个业务。
异步:执行A业务的时候,B业务也在同时执行。
++++++++++++++++++++
synchronized的一些特性:
①多个对象多个锁:关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。当线程和业务对象属于一对一关系,不存在争抢时,运行结果是异步的。这种情况下,不会出现非线程安全问题。只有当线程和业务对象属于多对一关系,执行相同对象的同步方法时,为了避免非线程安全问题,才用synchronized。
②当两个线程访问同一个对象的方法时,如果A线程先持有object对象的Lock锁,B线程可以异步调用object对象中的非synchronized方法。
③如果在X对象中使用了synchronized修饰非静态方法,则X对象就被当成锁。
④多个线程执行同一个对象的不同名称的同步方法或同步代码块时,是按顺序同步调用。
⑤synchronized支持重入锁。是指:自己可以再次获取自己的内部锁。比如,一个线程获得了某个对象锁,这个对象锁还没有释放。此时,它还可以再次获取这个对象锁。
⑥线程出现异常,会自动释放其持有的锁。
⑦重写方法如果不使用synchronized,就是非同步方法。
⑧synchronized方法是将当前对象作为锁。synchronized代码块是将任意对象作为锁。锁就是一个标识,哪个线程有这个标识,就可以执行同步方法。
+++++++++++++++++++++++++++
synchronized存在的弊端:
比如,A线程调用同步方法执行一个长时间任务,那么B线程等待的时间就比较长。此时,可用synchronized代码块来提高运行效率。
synchronized代码块间的同步性。当一个线程访问object对象的一个synchronized(this) 同步代码块时,其他线程无法访问同一个object中所有其他synchronized(this) 同步代码块。因为synchronized 使用的对象监视器是同一个。
System.out.println(…)方法就是用了synchronized(this) 同步代码块。
+++++++++++++++++++++++++++++++
synchronized 同步方法的作用:
①同一时间只有一个线程可以执行synchronized 同步方法中的代码。
②对其他synchronized 同步方法或synchronized(this)同步代码块调用呈同步效果。
synchronized(this)同步代码块的作用:
①同一个时间只有一个线程可以执行synchronized(this)同步代码块的代码。
②对其他synchronized 同步方法或synchronized(this)同步代码块调用呈同步效果。
-------------------------------------
2、volatile
volatile的使用场景:当想实现一个变量的值被更改时,让其他线程能取到最新的值时,就要对变量使用volatile。volatile只能修饰变量。主要作用是:让其他线程能看到最新的值。volatile 可以解决多线程出现的死循环问题。
volatile三个特征:可见性、原子性、禁止代码重排序。
1)可见性:B线程能马上看到A线程更改的数据。
2)原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
volatile的意义:
①使用volatile 解决多线程出现的死循环。
②实现原子性操作。
③禁止代码重排序。
3、Lock对象
使用Lock对象实现同步。主要有两个类:ReentrantLock和ReentrantReadWriteLock。Lock对象在功能上比synchronized更加丰富。
++++++++++++++++++++++++++++++++++++++
使用ReentrantLock类
ReentrantLock类有实现线程同步、嗅探锁定、多路分支通知等功能。
ReentrantLock实现同步的用法:调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。这两个方法必须成对使用。把想要实现同步的代码放在这两个方法之间即可。
synchronized结合wait()、notify()/notifyAll()可以实现wait/notify机制。ReentrantLock类借助Condition对象的await()和signal()也可以实现wait/notify机制。Condition对象的作用是控制并处理线程的状态,它可以使线程呈wait状态,也可以让线程继续运行。使用notify()方法进行通知时,被通知线程由JVM进行选择。notifyAll()会通知所有waiting线程,没有选择权,会有效率问题。但用ReentrantLock+Condition类可以实现“选择性通知”。
await()方法暂停线程运行的原理:
因为源代码内部执行了Unsafe类中的public native void park(Boolean isAbsolute, long time)方法,让当前线程呈暂停状态,方法参数isAbsolute代表是否为绝对时间,参数time代表时间值。如果isAbsolute为true,则time的单位为毫秒。如果isAbsolute为false,则time的单位为纳秒。
ReentrantLock类的缺点:
与ReentrantReadWriteLock类相比,ReentrantLock对象的所有操作都同步,所以哪怕只对实例变量进行读操作,这样会耗费大量的时间,降低运行效率。
++++++++++++++++++++++++++++++++++++++
使用ReentrantReadWriteLock类
ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率非常低下。所以,诞生了一种读写锁—ReentrantReadWriteLock类。使用它时,可以在读操作时不需要同步执行,加快运行速度,提升效率。
读写锁有两个锁:一个是读操作相关的锁,称共享锁;一个是写操作相关的锁,称排他锁。读写互斥,写读互斥,写写互斥,读读异步。只要有写锁,就会出现互斥同步。
4、线程间的通信
1、wait/notify机制
wait/notify机制类似于厨师和服务员的交互发生在“菜品传递台”上。服务员等着取菜,服务员就是wait。厨师把菜放在传递台上就是notify。
机制原理:
拥有相同锁的线程才可以实现wait/notify机制。
wait()方法是Object类的方法。wait()方法的作用是使当前执行wait()方法的线程暂停运行,在wait处暂停,并释放锁,直到接到通知或被中断为止。使调用该方法的线程从运行态转为wait态,等待被唤醒。只能在同步方法或同步代码块中调用wait()方法。如果调用wait()时,当前线程没有持有适当的锁,则异常。唤醒线程继续执行wait()方法后面的代码时,对线程的选择是按照执行wait()方法的顺序确定的,且需要重新获得锁。
notify()方法的作用是通知那些可能等待该锁的线程继续运行。如果有多个线程等待,则按照执行wait()的顺序仅唤醒等待同一个锁的“一个”线程,并使该线程重新获得锁。notify()方法只能在同步方法或同步代码块中调用。如果调用notify()方法时,线程没有持有适当的锁,则异常。注意,①执行notify()后,当前线程不会马上释放该锁,呈wait状态的线程也不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,即退出synchronized同步区后,当前线程才会释放锁,而呈wait状态的线程才可以获取该对象锁。②如果发出notify操作时,没有处于wait态的线程,那么该命令会被忽略。
notifyAll()方法的作用是按照执行wait()方法相反的顺序依次唤醒全部线程。
2、wait()、sleep()、notify()
wait():立即释放锁
sleep():不释放锁
notify():不立即释放锁
3、线程状态的切换
线程进入runnable状态的情况大体分4种:
①调用了sleep()后,时间超过了指定的休眠时间。
②线程正在等待某个通知,其他线程发出来通知。
③处于挂起状态的线程调用了resume恢复方法。
④线程成功获得了锁。
出现阻塞的情况大体分5种:
①线程调用了sleep(),主动放弃CPU。
②线程等待某个通知(notify)。
③程序调用了suspend()方法将该线程挂起。
④线程试图获取一个同步锁,但这个同步锁正被其他线程所持有。
⑤线程调用了阻塞式I/O方法,在该方法返回前,该线程被阻塞。
4、通过管道进行线程间通信
java提供了4个类进行线程间通信:PipedInputStream、PipedOutputStream、PipedRead、PipedWriter。管道流用于在不同线程间直接传输数据。
5、join()
x.join()方法的作用:使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码,具有串联执行的效果。join()方法具有使线程排队运行的效果。在使用join()方法的过程中,如果当前线程对象被中断,则当前线程出现异常。
join()方法与synchronized的区别是join()方法在内部使用wait()方法进行等待,而synchronized关键字使用锁作为同步。
x.join(long):参数用于设定等待时间。不管x线程是否执行完毕,时间到了并且当前线程z重新获得锁,则当前线程z会继续向后运行。如果没有重新获得锁,则一直尝试,直到获得锁为止。
join()和join(long)一旦执行,说明源代码中的wait(time)已经被执行,也就证明锁被立即释放,仅仅在指定的join(time)时间后,当前线程会继续向下执行。
+++++++++++++++++++++++++++++++++++++
join(long)和sleep(long)方法的区别
join(long)方法的功能是在内部使用wait(long)方法来进行实现,所以join(long)方法具有释放锁的特点。从源代码中可知,当执行wait(long)方法后当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。
sleep(long)方法是不释放锁的。
5、ThreadLocal类
基本介绍
变量值的共享可以用public static的变量实现。所有线程都使用同一个public static 变量。但ThreadLocal类可以实现每一个线程都有自己的变量。ThreadLocal类解决的是变量在不同线程的隔离性,即不同线程拥有自己的值。
ThreadLocal类的主要作用是将数据放入当前线程对象的ThreadLocalMap对象中,这个Map是Thread类的实例变量。ThreadLocal类自己不管理,不存储如何数据。它只是数据和Map之间的桥梁,用于将数据放入Map中。
ThreadLocalMap中的key存的是ThreadLocal对象,value就是存储的值。每个Thread中的Map只对当前线程可见,其他线程不可以访问当前线程对象中的ThreadLocalMap值。当前线程销毁,Map随之销毁。Map中的数据如果没有被引用,没有被使用,则随时GC。由于Map中的key不可重复,所以一个ThreadLocal对象对应一个value。
线程、Map、数据相当于:人(Thread)随身带着口袋(Map),口袋(Map)里面有东西(value)。
ThreadLocal类不能实现值继承。使用InheritableThreadLocal类可以提现值继承特性。
ThreadLocal类存取数据流程分析
ThreadLocal对象的set(…)方法原理是
1、获取当前线程对象的ThreadLocal.ThreadLocalMap对象。
2、如果ThreadLocalMap不为null,则ThreadLocalMap对象调用set(…)方法,存数据。
3、如果ThreadLocalMap为null(即第一次向其存放数据时),则调用createMap(…)方法,来创建ThreadLocal.ThreadLocalMap对象,并把第一次存放的key-value值放进去。
ThreadLocal对象的get(…)方法原理是:
1、获取当前线程对象的ThreadLocal.ThreadLocalMap对象。
2、如果ThreadLocalMap对象不为null,则调用该Map对象的getEntry(this)方法,以this为key,获得对应的Entry对象。如果该Entry对象不为null,则返回对应的value值。
3、如果ThreadLocalMap对象为null,则调用setInitialValue()方法,而此方法会调用initialValue()方法,最终返回initialValue()方法的返回值。
如果从未在Thread中的ThreadLocalMap中存数据,则调用get()方法返回的是null。如果不想返回null,则需要创建一个继承ThreadLocal类的子类,在子类中覆盖initialValue()方法。因为ThreadLocal.java中的initialValue()方法默认返回null。
为什么不能直接向ThreadLocal类中的ThreadLocalMap对象存取数据??
因为这是不能实现的。Thread类中的ThreadLocal.ThreadLocalMap 类型的threadLocals变量默认是包级访问,所以不能直接从外部访问该变量。只有同包中的类可以访问threadLocals变量。而ThreadLocal和Thread恰好在同一个包中。所以,外部代码可以通过ThreadLocal访问Thread类中的秘密对象—ThreadLocalMap。
6、多线程基础综述
使用多线程就是在使用异步。使用多线程技术时,代码的运行结果与代码的执行顺序无关。多线程随机输出的原因是:CPU将时间片分给不同的线程,线程得到时间片就执行任务。所以,线程在交替执行并输出。
使用多线程技术的场景:
①阻塞。一旦系统中出现了阻塞现象,则可以根据实际情况来使用多线程来提高运行效率。
②依赖。有A和B两个业务。当A业务发生阻塞时,B业务的执行不依赖A业务的执行结果,这时可用多线程来提高运行效率。
实现多线程的方式:
①继承Thread类。使用继承Thread类创建新线程时,最大局限是不支持多继承。为了支持多继承,可以实现Runnable接口。
②实现Runnable接口。如果想创建的线程类已经有一个父类了,就需要实现Runnable接口。它可以间接地实现“多继承”效果。
使用Runnable接口实现多线程的优点是:改善了使用继承Thread类的方式开发多线程应用在设计上的局限性。
Thread类也实现了Runnable接口,它们之间具有多态关系。
thread.start()、thread.run()
用start()方法来启动一个线程,线程启动会自动调用线程对象中的run()方法,run()方法里的代码就是线程对象要执行的任务,是线程执行任务的入口。main线程执行start()方法时,不会等start()方法,而是立即继续执行start()方法后面的代码。
start()耗时的原因:执行了多个步骤:
①通过JVM告诉操作系统创建Thread。
②操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread对象。
③操作系统对Thread对象进行调度,以确定执行时机。
④Thread在操作系统中被执行。
二者的区别:
thread.start()是启动新线程,是异步执行,执行run()方法的时机不确定。thread.run()是不启动新线程,是同步执行,立即执行run()方法。
执行start()方法的顺序不代表执行线程对象中的run()的顺序。
Thread类的常用方法
1、isAlive():判断线程是否处于活动状态,即已经启动且尚未终止的状态。
2、sleep(long millis):在指定的毫秒内让当前“正在执行的线程”暂停执行,指的是this.currentThread()返回的线程。
3、getId():获取线程的唯一标识。
4、interrupted():测试当前线程是否已经是中断状态,执行后会清除状态标志,所以,连续两次调用该方法,第二次调用interrupted()方法返回false。
5、isInterrupted():测试Thread对象是否已经是中断状态,不清除状态标志。
6、yield():放弃当前的CPU资源,让其他任务去占用CPU执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
7、setPriority():设置线程优先级。CPU尽量将执行资源让给优先级较高的线程。
--------------------------------------------------
suspend():暂停线程。resume():恢复线程的执行。使用这两个方法的缺点是:①独占。如果这两个方法使用不当,极易造成公共同步对象被独占,其他线程无法访问公共同步对象的结果。②数据不完整。容易出现线程暂停,进而导致数据不完整。
暂停线程意味着此线程还可以恢复运行。
Java中可以使正在运行的线程终止运行的三种方法:
①使用退出标志使线程正常退出。
②使用stop()强制终止线程,但不推荐。
③使用interrupt()方法中断线程。调用interrupt()方法仅仅是在当前线程中做了一个停止标记,并不是真正停止线程。
饿汉模式、懒汉模式
饿汉模式(立即加载)是指使用类的时候已经将对象创建完毕。常见的实现方法是直接用new实例化。在饿汉模式中,调用方法前,实例已经被工厂创建了。
懒汉模式(延迟加载)是指调用get()方法时实例才被工厂创建。常见的实现方法是在get()方法中进行new实例化。在懒汉模式中,调用方法时,实例才被工厂创建。
以上是关于学习笔记之多线程笔记的主要内容,如果未能解决你的问题,请参考以下文章
Python学习笔记18:标准库之多进程(multiprocessing包)