Java多线程、并发编程知识点总结 本文关键词:知识点,并发,多线程,编程,Java
Java多线程、并发编程知识点总结 本文简介:1、线程的状态1.1创建线程的两种方式,接口和线程类。利用接口的好处:更好的体现面向对象的思想,可以避免由于Java的单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;(同步问题)适合多个相同程序代码的线程区处理同一资源的情况。(关注微信订阅号:javaedu)1
Java多线程、并发编程知识点总结 本文内容:
1、线程的状态
1.1创建
线程
的两种方式,接口和线程类。利用接口的好处:更好的体现面向对象的思想,可以避免由于Java的单继承特性而带来的局限;
增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;(同步问题)适合多个相同程序代码的线程区处理同一资源的情况。(关注微信订阅号:javaedu)
1.2线程就绪等待调度运行start()方法。
1.3线程的中断
这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行。
1.4、线程挂起和恢复(挂起还拥有对象锁,死锁)
线程的挂起和恢复实现的正确方法是:通过设置标志位,让线程在安全的位置挂起
1.5
利用多线程模拟同步运行用jion方法,mThread.jion()表示该线程运行完毕后,在运行调用它的线程。
1.6
sleep
休眠
当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;
1.7stop线程停止
stop方法突然终止线程(持有这些锁必定有某种合适的理由——也许是阻止其他线程访问尚未处于一致性状态的数据,
突然释放锁可能使某些对象中的数据处于不一致状态)
1.8线程可以阻塞于四种状态:(参考资料:t.cn/RA5iKhq)
当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;
当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)
线程阻塞与不同I/O的方式有多种。常见的一种方式是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间
线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。
2、线程的种类
守护线程与线程阻塞的四种情况
Java中有两类线程:User
Thread(用户线程)、Daemon
Thread(守护线程)
用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。
守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。
因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。、
setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。
在守护线程中产生的新线程也是守护线程。
不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
3、线程所操作的数据
同步问题:
4、可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。
线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。
获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
某一个持有同步对象锁的线程可以多次进入这个同步代码块或方法。即同步对象锁可以重入!
同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。
6、Java内存模型
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。(本地内存+共享主存)
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。
这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
而volatile关键字就是提示JVM:对于这个成员变量,不能保存它的私有拷贝,而应直接与共享成员变量交互。
volatile型变量的特殊规则:
1、保证此变量对所有线程的可见性。需要注意,volatile变量的写操作除了对它本身的读操作可见外,volatile写操作之前的所有共享变量均对volatile读操作之后的操作可见
2、禁止指令重排序优化
final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步
因此在编码时,不需要将long和double变量专门声明为volatile。
主内存与工作内存
Java内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
根据Java虚拟机规范的规定,volatile变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性规定——从工作内存中读写数据前,
必须先将主内存中的数据同步到工作内存中,所有看起来如同直接在主内存中读写访问一般,因此这里的描述对于volatile也不例外
不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
对一个变量执行unlock操作之前,必须先把此变量同步回主内存(
7、轻量级同步
volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
使用建议:在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
假如有两个线程分别读写volatile变量时,线程A写入了某volatile变量,线程B在读取该volatile变量时,便能看到线程A对该volatile变量的写入操作,
关键在这里,它不仅会看到对该volatile变量的写入操作,A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,都将立即变得对B线程可见。
happen—before规则介绍
其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。
线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
8、同步锁说明
当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,
这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
类的每个实例都有自己的对象级别锁
访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,
因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。
持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。
使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。
类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
10、多线程环境中安全使用集合API
最初设计的Vector和Hashtable是多线程安全的。
在Collections类
中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合:
public
static
List
synchronizedList(list
l)
List
list
=
Collection.synchronizedList(new
ArrayList());
注意,ArrayList实例马上封装起来,不存在对未同步化ArrayList的直接引用(即直接封装匿名实例)。
这是一种最安全的途径。如果另一个线程要直接引用ArrayList实例,它可以执行非同步修改。
从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
、volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count
=
count+1”。
当且仅当满足以下所有条件时,才应该使用volatile变量:
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。
三、死锁问题
遵循以下原则有助于规避死锁:
1、只在必要的最短时间内持有锁,考虑使用同步语句块代替整个同步方法;
2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂;
3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁;
四、线程通信
在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。
notify()
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。
notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),
但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,
其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。
这与下面的notifyAll()方法执行后的情况不同。
notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),
变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。
如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,
直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
总结:在使用线程的等待/通知机制时,一般都要配合一个boolean变量值(或者其他能够判断真假的条件),在notify之前改变该boolean变量的值,让wait返回后能够退出while循环
(一般都要在wait方法外围加一层while循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。
五、并发新特性
1、
一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,
因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。
服务端面向连接:public
static
ExecutorService
newFixedThreadPool(int
nThreads)
Executor执行Runnable任务
Executor执行Callable任务
在Java
5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,
但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable
task)
方法来执行,
并且返回一个
Future,是表示任务等待完成的
Future。
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是
Runnable
不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,
而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,
则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
2、自定义线程池
ThreadPoolExecutor类创建,它有多个构造方法来创建线程池
BlockingQueue