文章目录
本文摘自《实战Java高并发程序设计》,了解详细内容推荐购买原版书籍。
第二章 Java并行程序基础
2.1有关线程你必须知道的事
进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。是线程的容器,程序的实体。
线程:轻量级进程,是程序执行的最小单位。
使用多线程而不是多进程进行并发程序设计,因为线程之间切换和调度成本远小于进程。
NEW状态表示刚创建的线程,还未执行。Start()方法调用,才表示线程开始执行。执行时线程处于Runnable状态,表示线程所需资源已经备好。如果在执行的过程中遇到了synchronized同步块,则进入Blocked阻塞状态,线程暂停执行,直到获得请求的锁。Waiting和Timed_Waiting都表示等待状态,区别是Waiting会进入无时间限制的等待,TW有时限。一般,Waiting线程是在等待一些特殊事件。一旦等到,线程再次执行,进入Runnable状态。当线程执行完毕后,则进入Terminated状态,表示结束。
2.2初始线程:线程的基本操作
2.2.1新建线程
Thread t1 = new Thread();
t1.start();
start()方法新建线程并让这个线程执行run()方法。
Thread t1 = new Thread();
t1.run();
在当前线程中调用run()方法。
注意:不要用run()来开启新线程,他只会在当前线程中,串行执行run()中的代码。
Tread t1 =new Thead(){
@Override
public void run(){
System.out.println("HEllo, i am t1");
}
};
t1.start();
匿名内部类,重载run()方法。
考虑到Java是单继承的,继承本身也是一种很宝贵的资源,所以可以使用runnable接口实现同样操作。Runnable是一个单方法接口,只有一个run()方法。
单继承的意思是,一个子类不能有两个以上的父类。如果你想写一个类C,但这个类C已经继承了一个类A,此时,你又想让C实现多线程。用继承Thread类的方式不行了。(因为单继承的局限性)。此时,只能用Runnable接口。
public interface Runnable {
public abstract void run();
}
此外,Thread类还有一个重要的构造方法:
public Thread(Runnable target)
public void run(){
if (target!=null){
target.run();
}
}
传入一个Runnable接口的实例,在start()方法调用时,新的线程就会执行Runnable.run()方法。
注意:默认的Thread.run()就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该干什么,更为合理。
public class CreateThread3 implements Runnable {
public static void main(String[] args) {
Thread t1 = new Thread(new CreateThread3());
t1.start();
}
@Override
public void run() {
System.out.println("oh,i am Runnable!");
}
}
上述代码实现runnable接口,并将该实例传入Thread。避免了重载Thread.run(),单纯使用接口定义Thread,是最常用的方法。
2.2.2终止线程
Thread有一个stop()方法,但是太过暴力,强行把执行到一半的线程终止,释放线程所持有的锁,可能会引起一些数据不一致的问题。
可以增加一个stopMe()方法,增加stopme标记。这样就没机会写坏对象了。
public static class ChangeObjectThread extends Thread {
volatile boolean stopme = false;
public void stopMe(){
stopme = true;
}
@Override
public void run(){
while(true){
if (stopme) {
System.out.println("exit by stopme");
break;
}
synchronized (u) {
int v = (int) (System.currentTimeMillis() / 1000);
u.setId(v);
//Oh,do sth .else
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
2.2.3线程中断
中断表面上就是让目标线程停止执行的意思,实际上并非如此。
线程中断不会使线程立即退出,而是给线程发送一个退出通知。至于目标线程收到后如何处理,则由目标线程自行决定。
与线程中断有关的三个方法:
public void Thread.interrupt()
public boolean Thread.isInterrupt()
public static boolean Thread.interrupted()
Thread.interrupt()方法是一个实例方法。通知目标线程中断,也就是设置中断标志位表示当前线程已经被中断了。Thread.isInterrupt()方法也是实例方法,判断当前线程有没有被中断(通过检查中断标志位)。静态方法Thread.interrupted()判断当前线程中断状态,同时清除中断标志位状态。
如果希望程序在中断后退出,就必须为它增加相应中断处理代码:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(){
@Override
public void run(){
while (true) {
if (Thread.currentThread().isInterrupted()){
System.out.println("Interrupted!");
break;
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
Thread.sleep()函数签名如下:
Public static native void sleep(long millis) throws InterruptedExpection
“A native method is a Java method whose implementation is provided by non-java code.”
Thread.sleep()方法会让当前线程休眠,会抛出一个InterruptedExpection中断异常。InterruptedExpection不是运行时异常,也就是说程序必须捕获并处理它,当线程在sleep休眠时被中断,这个异常就会产生。
public static void main(String[] args) throws InterruptedException {
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 Sleepp");
//设置中断状态
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
注意:Thread.sleep()方法由于中断抛出异常,此时,它会清除中断标记,如果不加处理,在下一次循环开始时,就无法捕获这个中断,所以在异常处理中,再次设置中断标记位。
2.2.4等待(wait)和通知(notify)
× 为了支持多线程之间协作,JDK提供了接口线程等待wait()方法和通知notify()方法。这两个方法不在Thread类中,而在输出Object类。所以任何对象都可以调用。
两个方法签名如下:
Public final void wait() throws InterruptedExpection
Public final native void notify()
线程A调用了obj.wait()方法后,就会停止继续执行,转为等待状态。一直等到其他线程调用了obj.notify()方法为止。Obj对象就变成了多个线程中有效通信手段。
Object.notify()被调用时,会从等待队列中随机选择一个线程唤醒。
Obj.notifyAll()方法可以唤醒所有等待线程。
Object.wait()必须包含在对应的synchroniz的语句中,无论是wait()或notify()都需要获得目标Object的一个监视器(锁)。
注意:sleep()和wait()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另个区别就是wait()方法会释放目标对象的锁,而sleep()不会释放任何资源。
2.2.5 挂起(suspend)和继续执行(resume)线程
× 被标记为废弃方法,不推荐使用。原因是suspend()挂起线程时,不会释放任何锁资源。如果resume()操作意外的在suspend()之前就执行了,那么被挂起的线程可能很难有机会被继续执行。更严重的是:它占用的锁不会释放,可能导致整个系统工作不正常。
被挂起的线程从状态上看,还是runnable,会影响对系统当前状态判断。
Jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,生成java虚拟机当前时刻的线程快照。
可以利用wait()和notify()方法在应用层面上实现suspend()和resume()功能。
给出一个标记变量suspendme,表示当前线程是否被挂起。增加suspendMe()和resumeMe()两个方法,用于挂起和继续执行线程。
2.2.6等待线程结束(join)和谦让(yield)
很多时候,一个线程的输入可能非常依赖于另一个或多个线程的输出,这就需要等待依赖线程执行完毕。JDK提供join()操作实现。
两个join()方法:
Public final void join() throws InterruptedExpection
Public final synchronized void join(long millis) throws InterruptedExpection
第一个表示无限等待,一直阻塞当前线程,直到目标线程执行完毕。
第二个给出最大等待时间,超过时间目标线程还在执行,当前线程“等不及”继续执行。
Join()的本质是让调用线程wait()在当前线程对象实例上。当线程执行完成后,被等待线程会在退出前调用notifyAll()通知所有等待线程继续执行。因此,值得注意的是:不要在应用程序中,在Thread对象实例上使用类似wait()或notify()等方法,这很可能影响系统API的工作,或被系统API影响。
Thread.yield()方法:
Public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出CPU,之后再进行CPU资源争夺。如果觉得一个线程不太重要,或者优先级非常低,又害怕它占用太多CPU资源,可以适当调用Thread.yield()方法。
2.3 volatile与Java内存模型(JMM)
* Java内存模型都是围绕原子性、有序性、可见性展开的。为了在特殊场合,确保线程之间的原子性、有序性、可见性。Java使用一些特殊操作或关键字来声明、告诉虚拟机,在这个地方要尤其注意,不能随便变动优化目标指令。关键字Volatile就是其中之一
保证写入数据不坏,最简单就是加入volatile声明,告诉编译器(虚拟机)这个数据要格外小心,因为他会不断被修改。
需要注意的是,volatile不能代替锁。也无法保证一些复合操作的原子性。
此外,volatile也能保证数据的可见性和有序性。例子:
在虚拟机client模式下,由于JIT没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出。这就是典型的可见性的问题。
注意:可以使用Java虚拟机参数-server切换到server模式。
可以用volatile来申明ready变量,告诉虚拟机这个变量可能在不同线程修改。
2.4 分门别类的管理:线程组
在一个系统中,如果线程数量比较多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组中。
线程组的使用:
其中activeCount()方法可以获得线程的总数,由于线程时动态的,这个值只是一个估计。.list()方法可以打印这个线程组的所有线程信息,对调试有帮助。
线程组有stop()方法,停止线程组中所有线程。使用时谨慎。
2.5 驻守后台:守护线程(Daemon)
守护线程在后台默默完成一些系统性的服务,比如,垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的是用户线程,可以认为是系统的工作线程,他会完成这个程序应该要完成的业务操作。如果用户线程全部结束,就意味着这个程序实际无事可做了。守护线程也没有存在必要了,那么程序就应该自然结束。因此,在Java应用内,只有守护线程时,虚拟机会自然退出。
守护线程使用:
注意:守护线程必须在线程start()之前设置,否则失败报错,但继续执行,被当为用户线程。
例子中t为守护线程,系统中只有主线程main为用户线程,因此,在main线程休眠2秒后退出时,整个程序也随之结束(只有守护线程)。如果不设置守护线程,t线程会一直打印。
2.6 先干重要的事:线程优先级priority
运气不好高优先级线程也可能抢占失败。Java中使用1-10(有效范围)表示线程优先级。高优先级的线程倾向于更快的完成。
一般可以使用内置三个静态标量表示:
2.7 线程安全的概念与synchronized
volatile并不能真的保证线程安全。它只能确保一个线程修改数据后,其他线程能够看到这个改动。但当两个线程同时修改某一数据时,依然会产生冲突。
期望最终结果为20000000,但最终值会小于20000000,。因为两个线程同时对i进行写入时,其中一个线程结果会覆盖另一个。(虽然i被声明为volatile变量)
要解决这个问题,要保证多个线程对i操作时完全同步。==在A线程写入时,B不仅不能写,同时也不能读。
关键字synchronized作用是实现线程间同步。它的工作是对同步的代码加锁,使得每一次只有一个线程能够进入同步块,从而保证线程的安全性。
synchronized用法:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
上述代码也可以写成如下形式:(作用于实例方法)
在进入increase()方法前,线程必须获得当前对象实例的锁。本例中就是instance对象。注意14、15行Thread创建方式。使用Runnable接口创建两个线程,两个线程都指向同一个Runnable接口实例(instance对象),这样保证两个线程工作时,关注同一个对象锁,保证线程安全。
一种错误的同步方式:
两个线程指向了不同的Runnable实例。修改方式:将synchronized作用于静态方法。方法块要请求当前类的锁而非当前实例。
2.8 程序中的幽灵:隐蔽的错误
2.8.1 无提示的错误案例
2.8.2 并发下的ArrayList
ArrayList线程不安全容器。
上述程序执行可能三种结果:
一, 正常结束。
二, 程序抛出异常。
ArrayList在扩容过程中,内部一致性被破坏,由于没有锁的保护,另一个线程访问到不一致的内部状态,导致出现越界。
三、出现一个隐蔽的错误,比如打印ArrayList大小。
1793758
* 多线程访问冲突,保存容器大小变量被多线程不正常访问,同时两个线程同时对一个位置赋值。
注意:改进方法:用线程安全的Vector替换ArrayList即可。
2.8.3 并发下诡异的HashMap
HashMap非线程安全。
上述代码期望得到map.size()=100000.实际上会有三种可能:
一、 程序正常结束,结果符合预期。
二、 程序正常结束,结果不对,小于100000.
三、 程序永远无法结束。
前两种情况和ArrayList类似,第三种情况时,打开任务管理器,会发现这段代码占用了极高的CPU,最有可能占用了两个CPU核,并使得这两个CPU占用率100%。这非常类似于死循环的情况。
使用jstack工具显示程序的线程信息。其中jps可以显示当前系统中所有的Java进程。而jstack可以打印给定Java进程的内部线程及其堆栈。
可以找到t1,t2和main线程:
可以看到,主线程main处于等待状态,并且这个等待是由于join()方法引起的,符合预期。而t1和t2线程都处于Runnable状态,并且执行当前语句为HashMap.put()方法。查看put()方法的第498行代码,如下:
可以看到,当前两个线程正在遍历HashMap内部数据。当前所处循环乍看是一个迭代遍历,就如同遍历一个链表一样。但是,由于多线程冲突,这个链表结构已经找到破坏,链表成环了!图2.9所示最简单一种环状结构。
此死循环问题在JDK8中已经不存在了。Java8对HashMap内部实现做了大规模调整,规避了这个问题。但是,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。
解决方法:使用ConcurrentHashMap。
2.8.4 初学者常见问题:错误的加锁
假设我么需要一个计数器,计数器会被多个线程同时访问:
Integer数据不可变对象。也就是说对象一旦被创建就不能被修改。
使用javap反编译这段代码的run()方法,可以看到:
19-22行实际上使用了Integer.valueOf()方法新建了一个Integer对象,并赋值给i。i++实际为i=Integer.valueOf(i.intValue()+1);查看Integer.valueOf(),可以看到:
Integer.valueOf()实际是工厂方法,倾向返回一个代表指定数值的Integer实例。在多线程之间,不一定能看到同一个对象(i对象一直在变)。加锁可能加在不同对象实例上。
修正方法:将synchronized(i)给为synchronized(instance)即可