Java并行程序基础
2.1 线程必知
进程:
- 资源分配最小单位
- 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段
线程:
- 程序执行的最小单位,轻量级进程
- 线程没有独立的地址空间,它使用相同的地址空间共享数据
- 一个进程里面可以有多个线程
为什么要使用线程?
- 创建一个线程比进程开销小
- CPU切换一个线程比切换进程花费小
- 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
- 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
- 缺点:多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间)
线程的生命周期
初始线程:线程的基本操作
新建线程
方法一:继承Thread
Thread t = new Thread(()->System.out.println("Hello,this is created by method1"));//需重载run()方法
t.start();
不要用run()启动线程,注意调用start()和run方法的区别
方法二:实现Runnable接口
实现原理:Thread.run()方法直接调用Runnable实现类对象的run()方法,静态代理
public class CreateThread implements Runnable{
@Override
public void run(){
System.out.println("Hello,this is created by method2");
}
public static void main(String[] args){
Thread t = new Thread(new CreateThread());
t.start();
}
}
方法三:使用Callable和future创建带返回值的线程
创建并启动有返回值的线程的步骤如下:
(1)创建。创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
(2)封装。使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
(3)启动。使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
(4)获取返回值。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class myThread3 implements Callable {
@Override
public String call() throws Exception {
return "hello";
}
public static void main(String[] args) {
FutureTask futureTask = new FutureTask<>(new myThread3());
Thread t3 = new Thread(futureTask);
t3.start();
try {
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
终止线程
- 方法一:stop(),被废弃不推荐使用
为什么呢?结束线程时,会直接终止线程,会引起数据的一致性问题。可以参考TCP的四次挥手机制中,服务端收到客户端的FIN后,并不会立即关闭自己的服务,而是先发送ACK,将自己这边还未发送的数据继续发送,然后再发送FIN
使用stop()而引起的数据不一致的问题:
public class StopThreadUnsafe{
public static User u = new User();
public static class User{
private int id;
private String name;
public User(){
id=0;
name="0";
}
//省略get,set和toString方法
}
public static class ChangeObjectThread extends Thread{
@Override
public void run(){
whie(true){
synchronized(u){
int v = (int)(System.currentTimeMills()/1000);
u.setId(v);
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
u.setName(v);
Thread.yield();
}
}
}
}
public static class ReadObjectThread extends Thread{
@Override
public void run(){
whie(true){
synchronized(u){
if(u.getId()!=Integer.parseInt(u.getName())){
System.out.println(u.toString());
}
Thread.yield();
}
}
}
}
public static void main(String[] args){
new ReadObjectThread().start();
while(true){
Thread t = new ChangeObjectThread();
t.start();
Thread.sleep(150);
t.stop();
}
}
}
如何正确停止线程呢?利用volatile修饰的标志位
public static class ChangeObjectThread extends Thread{
volatile boolean stopme = false;
public void stopMe(){
stopme = true;
}
@Override
public void run(){
whie(true){
if(stopme){
System.out.println("exit by stop me");
break;
}
synchronized(u){
int v = (int)(System.currentTimeMills()/1000);
u.setId(v);
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
u.setName(v);
Thread.yield();
}
}
}
}
线程中断
- 一种重要的线程协作机制,并不是使线程立即退出,而是给线程一个通知,告诉目标线程,有人需要你退出了,目标线程收到中断通知后,如何处理自行决定
- 与中断有关的三个方法:
public void Thread.interrupt()//中断线程
public boolean Thread.isInterrupt()//判断是否被中断
public static boolean Thread.interrupted()//判断是否被中断,并清除当前中断状态
- 与stopme()手法相似,但中断更厉害,因为wait()和sleep()这类操作也属于中断操作,能被isInterrupt()检测到
public static void main(String[] args){
Thread t1 = new Thread();
@Override
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interrupted!");
break;
}
}
try{
Thread.sleep(2000);
}catch (InterruptedException e){
System.out.println("Interrupted when sleep");
Thread.currentThread.interrupt();
}
Thread.yield();
}
t1.start();
Thread.sleep(2000);//让当前线程休眠若干时间,抛出一个InterruptedException中断异常
t1.interrupt();
}
注意:Thread.sleep()方法由于中断而抛出异常,在捕获异常后,该中断标志位会被清除,因此要再次设置中断标志位,以让下一次循环的开始检测到中断
等待(wait)和通知(notify)
- 属于Object类的实例方法
public final void wait() throws InterruptedException
public final native void notify()
wait()和notify()是什么:wait()必须与synchronized搭配使用,当一个对象调用wait()方法,则这个对象所处的线程就会进入object对象的等待队列,在这个等待队列中,可能有多个线程都在等待同一个object对象,当object.notify()调用后,就会从等待队列中(完全)随机选择一个线程将其唤醒
wait()和notify()的工作流程:
- 假设有两个线程T1和T2
- T1在执行wait()方法前,先通过synchronized获取object对象的监视器,在wait()方法执行后,释放这个监视器
为什么要释放监视器?
目的是使得其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行 - T2在notify()调用前,也必须获得object对象的监视器,由于T1已经释放了监视器,所以T2可以顺利获得,T2执行notify()方法唤醒等待线程
- T1被唤醒后并不会立即去执行后续的代码,而是先尝试重新获取object的监视器,若无法获得,则必须等待这个监视器,当获得这个监视器后,T1开始执行后续代码
案例:
public class NotifyTest {
public final static Object object=new Object();
public static class T1 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println(System.currentTimeMillis()+":T1 start");
System.out.println(System.currentTimeMillis()+": T1 wait for object");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+": T1 end");
}
}
}
public static class T2 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println(System.currentTimeMillis()+":T2 start notify object");
object.notify();
System.out.println(System.currentTimeMillis()+":T2 end");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1=new T1();
Thread t2=new T2();
t2.start();
t1.start();
}
}
Object,.wait()和Thread.sleep()都能让线程等待若干时间,wait()方法需要被唤醒,在唤醒后hi释放目标对象的锁,而sleep()不会释放任何资源
挂起(suspend)和继续执行(resume)
- 废弃不推荐使用
- 如果rusume在suspend前执行,那么被挂起的线程所占用的锁不会被释放,并且其状态为Runnable,影响对当前状态的判断
例子说明:
如何实现一个可靠的suspend操作呢?利用wait()和notify()
notifyAll():唤醒等待队列中的所有线程,让他们竞争锁
等待线程结束(join)和谦让(yield)
join
- 应用场景:当一个线程的输入可能依赖于另外一个或多个线程的输出时,当前线程就需要等待依赖线程执行完毕,才能执行
- jdk提供的两个join()操作
//无限期等待,当前线程在目标线程执行完毕前一直阻塞,当前线程一直等着目标线程完毕
public final void join() throws InterruptedException
//当前线程会在规定时间内等待目标线程,一旦超过规定时间,当 前线程就不等了,继续向下执行
public final synchronized void join(long millis) throws InterruptedException
- 实例程序:
- join的本质:让调用线程wait()在当前线程对象实例上
while(isAlive()){ wait(0); }
yield
public static void yield();
使当前线程让出CPU后继续争夺资源
volatile与Java内存模型(JMM)
volatile
- volatile无法保证一些符合操作的原子性,例如i++;
- 可以保证数据的可见性和有序性
分门别类的管理:线程组
驻守后台:守护线程(Daemon)
- 什么是守护线程?当一个Java应用中只有守护线程时,JVM就会退出
- 注意:setDaemon需在start之前执行
先干重要的事:线程优先级
- 线程优先级高的并不一定先执行,只是拥有更多可以执行的机会
线程安全的概念与synchronized
- 什么是线程安全?
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的
- 线程不安全的例子:
- synchronized介绍
作用:实现线程间的同步
工作:对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性
- synchronized的多种用法
指定加锁对象:
直接作用于实例方法:
直接作用域静态方法:
synchronized实现线程安全的i++,保证复合操作的原子性
注意:使用Runnable让两个线程关注一个同一个对象锁
错误示范:
使用synchronized的第三种方法进行修正
- synchronized除了线程同步、确保线程安全,还可以保证线程间的可见性和有序性(多个线程串行执行)
程序中的幽灵:隐蔽的错误
出现异常的错误至少可以发现,没有异常的错误例如数据溢出就很难排查
无提示的错误案例
并发下的ArrayList
例子:
可能出现的三种情况
- 程序正常结束
- 程序抛出数组越界异常
- 没有提示的错误
改进方法:使用线程安全的Vector代替ArrayList
并发下诡异的HashMap
HashMap的源码解析
Jdk 8以前为什么 HashMap在多线程下的put操作容易导致链表成环
初学者常见问题:错误的加锁
public class BadLockOnInteger implements Runnable{
public static Integer i=0;
static BadLockOnInteger instance = new BadLockOnInteger();
@Override
public void run(){
for(int j=0;j<10000000;j++){
synchronized(i){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t1 = new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
为什么最终的结果不是20000000?
在Java中Integer属于不变对象,每次对Integer对象进行加法操作时,实际上是新创建了一个Integer对象来表示加完后的结果,因此i++的本质是创建了一个新的Integer对象,并将其引用赋给i,所以在多个线程间,并不一定能够看到同一个对象,代码中两个线程每次加锁可能都在了不同的对象实例上
如何修正?
synchronized(instance)