整理参考:
1、Java核心技术卷1
2、Java多线程编程核心技术
3、CDSN:Java多线程学习
4、CSDN:jvm主内存和工作内存
多任务:同一刻运行多个程序的能力。
多线程:一个程序同时执行多个任务。通常每一个任务称为一个线程(thread),是线程控制的简称。
区别:每个进程拥有自己的一整套变量,而线程之间共享数据。
为什么多线程:线程是轻量级进程,是程序执行的最小单位。线程之间的切换和调度成本远小于进程。
将任务代码移到实现了Runnable接口的类的run方法中。Runnable接口非常简单,只有一个方法:
public interface Runnable{
void run();
} Runnable是一个函数式接口,可以用lambda表达式建立一个实例:Runnable r=()->{task code};
由Runnable创建一个Thread对象,启动线程:
Thread t=new Thread(r);
t.start();也可以通过构建一个Thread类的子类定义一个线程,然后构造一个子类的对象,并调用start方法。不过这种方法不再推荐,应该将要并行运行的任务和运行机制解耦合。
class MyThread extends Thread{
public void run(){
task code;
}
}注意:直接调用Thread类或Runnable对象的run方法,只会执行同一个线程中的任务,而不会启动新线程。而调用Thread.start方法,会创建一个执行run方法的新线程。
待整理。
| 方法名 | 作用 |
|---|---|
| currentThread() | 返回当前正在执行的线程对象的引用 |
| getId() | 返回此线程的标识符 |
| getName() | 返回此线程的名称 |
| getPriority() | 返回此线程的优先级 |
| setPriority() | 更改线程的优先级 |
| isAlive() | 测试此线程是否活动状态(线程处于正在运行或准备运行的状态) |
| sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂时停止 |
| interrupt() | 中断这个线程 |
| interrupted() | 测试线程是否处于中断状态,并将状态标志改为false |
| isInterrupted() | 测试线程是否处于中断状态,但不修改状态标志 |
| setName(String name) | 将线程改名 |
| isDaemon() | 测试线程是否为守护线程 |
| setDaemon(boolean on) | 将线程标记为守护线程 |
| join() | 等待线程终止,如子线程调用该方法,主线程需等子线程执行结束才能执行 |
| yield() | 放弃当前CPU资源(但是放弃时间不确定,可能会重新获得CPU时间) |
当线程的run方法执行完方法体中最后一句语句,并经由执行return语句返回时,或者出现了方法中没有捕获的异常时,线程将终止。
没有可以强制线程终止的方法。但是,interrupt方法可以请求终止线程。
当对一个线程调用interrupt方法时,线程的中断状态将被置位。每个线程应该不时地检查这个标志。
while(!Thread.currentThread().isInterrupted() && more work to do){
do more work
}如果线程被阻塞,就无法检测终端状态,当一个被阻塞的线程(调用sleep或wait)调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。
- 新创建:如new Thread(r),该线程还没有开始运行。
- 可运行:一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行。
- 被阻塞和等待:
- 线程试图获取内部对象锁,而锁被其他线程占用,线程进入
阻塞状态 - 当线程等待另一个线程通知调度器一个条件时,进入
等待状态 - 有几个方法有一个超时参数,调用它们导致线程进入
计时等待状态
- 线程试图获取内部对象锁,而锁被其他线程占用,线程进入
- 被终止:run方法正常退出或没有捕获的异常终止了run方法。
每一个线程有一个优先级,默认继承它的父线程的优先级。可以用setPriority()方法提高或降低任何一个线程的优先级(最小为1,最大为10,普通线程的优先级默认是5)。
通过调用t.setDaemon(true)将线程转为守护线程,守护线程唯一用途是为其他线程提供服务。当所有非守护线程结束之后,守护线程会被全部杀死。
最常见的守护线程:垃圾回收线程。
如果两个线程存取相同的对象,并且每一个线程都调用了一个修改对象状态的方法,会发生竞争条件(race condition)。
有两种机制可以防止代码块受并发访问的干扰。Java语言提供了一个synchronized关键字达到这一目的,synchronized关键字自动提供了一个锁以及相关的“条件”,并且JavaSE5.0引入了ReentrantLock类。
synchronized取得的锁是对象锁,如果多个线程访问的是同一个对象,哪个线程先执行带有synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈现等待状态。如果多个线程访问的是多个对象,多个对象会产生多个锁。
锁是可重入的,因为线程可以重复地获得已经持有的锁。当一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入,就会造成死锁。
class Test{
synchronized public void test1(){
System.out.println("test1");
test2();
}
synchronized public void test2(){
System.out.println("test2");
test3();
}
synchronized public void test3(){
System.out.println("test3");
}
}
public class TestSync {
public static void main(String[] args) {
Test t=new Test();
Thread thread=new Thread(()->t.test1());
thread.start();
}
}
//运行结果
test1
test2
test3 可重入锁也支持在父子类继承的环境中。当存在父子继承关系的时候,子类完全可以通过可重入锁调用父类的同步方法。
但是同步不能继承,所以还是需要子类方法中添加synchronized关键字。
使用synchronized关键字声明方法有些时候存在着一些弊端,比如一个线程A调用同步方法后获得锁,那么另一个方法就需要等待A执行完,但是如果A执行的是一个费时的任务,就会特别耗时。因此还有另外一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
public void method(){
other section
synchronized(obj){//this is the syntax for a synchronized block
critical section
}
} 当一个线程访问一个对象的synchronized同步代码块的时候,另一个线程仍然可以访问该对象的非synchronized同步代码块。
同步代码块的obj又称为对象监视器,当两个线程使用了同一个对象监视器的时候,运行结果是同步的;当使用不同的对象监视器的时候,运行结果就不是同步的了。
synchronized关键字如果加上static静态方法,或者使用synchronized(class)代码块,都是给Class类上锁,而synchronized关键字加到非静态方法方法上只是给对象加锁。
类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不相干的,但是每个类只有一个类锁。如果synchronized同时修饰了静态方法和实例方法,那么它们将会交替执行,因为类锁和对象锁是不一样的锁。(下面这个程序和它的执行结果说明了这一点)
public class TestSync{
public synchronized void test1(){
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try{
Thread.sleep(500);
}catch (InterruptedException e){}
}
}
public static synchronized void test2(){
int i = 5;
while( i-- > 0){
System.out.println(Thread.currentThread().getName() + " : " + i);
try{
Thread.sleep(500);
}catch (InterruptedException e){}
}
}
public static void main(String[] args){
final TestSync ts = new TestSync();
Thread test1 = new Thread(()->ts.test1(),"test1");
Thread test2 = new Thread(()->TestSync.test2(),"test2");
test1.start();
test2.start();
}
}
//运行结果
test1 : 4
test2 : 4
test2 : 3
test1 : 3
test1 : 2
test2 : 2
test1 : 1
test2 : 1
test2 : 0
test1 : 0另外,线程获得对象锁的同时,也可以获得类锁,两者可以并存。
要理解volatile关键字,首先理解下面这个代码:
class RunThread extends Thread{
private boolean isRunning=true;
int m;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
System.out.println(Thread.currentThread().getName());
this.isRunning=isRunning;
}
public void run(){
System.out.println(Thread.currentThread().getName());
while(isRunning==true) m+=1;
System.out.println(m);
System.out.println("Thread ends");
}
}
public class Run{
public static void main(String[] args) throws InterruptedException{
RunThread thread=new RunThread();
thread.start();
Thread.sleep(2000);
thread.setRunning(false);
System.out.println("isRunning="+thread.isRunning());
}
}
//运行结果
Thread-0
main
isRunning=false 可以看到,程序并没有输出“Thread ends”,也就是sleep结束之后,线程仍然在执行
while循环。这个问题的原因就在于Java的内存模型JMM,JDK1.2之后,JMM规定了所有的变量都存储在主内存中,而每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对于变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
所以就很好理解,虽然修改了isRunning的值,但是我们可以看到,在执行setRunning()方法的时候,虚拟机此时运行的线程是main线程,也就是说Thread-0线程的本地内存中的值其实并没有被改变,所以就导致了死循环。
解决办法就是加上volatile关键字,因为volatile关键字的语义是成员变量对不同线程的可见性(另外它还有一层语义就是防止程序的重排序),用volatile修饰的变量每次被线程访问的时候,都强迫从主内存中重读该变量的值,当变量发生变化的时候,强迫线程将变化值写回主内存,就相当与保持了数据对不同线程的同步可见性。
补充:在while循环中加入输出语句或者sleep方法也可以停止死循环,因为JVM会保证内存的可见性,当CPU有时间它就会尽力去保证变量的更新,但如果CPU一直被占用,就不能保证内存的可见性了。
- 性能:volatile关键字是轻量级的线程同步,不会发生阻塞;而synchronized可能引发阻塞,但是在JavaSE1.6之后,对锁进行了优化后,性能和volatile相差不大
- 作用域:volatile只能修饰变量;synchronized可以修饰方法已经代码块
- 同步安全:volatile并不能保证数据原子性,而synchronized可以保证多线程之间访问的同步性
原子性:一个操作是不可中断的,要么全部执行成功,要么全部执行失败。
因为volatile只能保证每次用数据的时候从主存开始读,然后再写回主存,但它并不能保证在读和写之间的空隙里,没有另外的线程访问该变量,所以volatile并不能保证变量的原子性,要保证原子性可以使用synchronized关键字。
public class TestVolatile {
private static volatile long count = 0;
public static void main(String[] args) {
TestThread t1 = new TestThread();
t1.start();
TestThread t2 = new TestThread();
t2.start();
while (t1.isAlive() || t2.isAlive()) {}
System.out.println("final count is: " + count);
}
private static class TestThread extends Thread {
public void run() {
long i = 0;
while (i++ < 10000000L) count++;
}
}
}
//运行结果
第1次:final count is: 7806434
第2次:final count is: 6771763
第3次:final count is: 8327083 因为volatile并不能保证原子性,所以除了使用synchronized对方法进行同步之外,还可以使用原子类AtomicInteger来实现修饰变量的同步。原子类可以在没有锁的情况下做到线程安全。
同样的,我们将上面一个证明volatile不具备原子性的例子中的变量改成原子类再执行,结果就是正确的,而且在线程方法中输出的count也是有序的。
import java.util.concurrent.atomic.*;
public class TestAtomic {
private static AtomicLong count = new AtomicLong(0);
public static void main(String[] args) {
TestThread t1 = new TestThread();
t1.start();
TestThread t2 = new TestThread();
t2.start();
while (t1.isAlive() || t2.isAlive()) {}
System.out.println("final count is: " + count);
}
private static class TestThread extends Thread {
public void run() {
long i = 0;
while (i++ < 10000000L) count.incrementAndGet();
}
}
}
//运行结果
第1次:final count is: 20000000
第2次:final count is: 20000000
第3次:final count is: 20000000但是,原子类也并不是绝对安全的。
import java.util.concurrent.atomic.AtomicInteger;
class TestThread extends Thread{
private static AtomicInteger count=new AtomicInteger(0);
public void run(){
for (int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+count.incrementAndGet());
}
}
}
public class Run{
public static void main(String[] args){
TestThread[] t=new TestThread[100];
for (int i=0;i<100;i++) t[i]=new TestThread();
for (int i=0;i<100;i++) t[i].start();
}
}
//运行结果(局部)
Thread-40 9974
Thread-40 9975
Thread-76 9959
Thread-76 9977
...
Thread-76 9989
Thread-40 9976
Thread-40 9991 可以发现,虽然最终的结果是正确的,但是在输出过程中出现了乱序的情况,这是因为虽然原子类的操作是原子性的,但是方法之间的调用不是原子性的,用javap -c指令将java的字节码调出来看可以知道,sout方法在调用的时候进行了14步操作,所以很容易让另外一个线程占用CPU时间片,从而导致乱序。如果在方法中存在逻辑关系,那么就很有可能因为乱序而导致线程不安全。
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
21: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
24: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: ldc #8 // String
29: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
32: getstatic #9 // Field count:Ljava/util/concurrent/atomic/AtomicInteger;
35: invokevirtual #10 // Method java/util/concurrent/atomic/AtomicInteger.incrementAndGet:()I
38: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
41: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 内部对象锁有一个相关条件,wait方法添加一个线程到等待集中,notify All/notify方法解除等待线程的等待状态。
| 方法 | 描述 | 调用 |
|---|---|---|
| notifyAll() | 解除那些在该对象上调用wait方法的线程的等待状态 | 同步方法或同步块中调用 |
| notify() | 随机选择一个在该对象上调用wait方法的线程,解除其等待状态 | 只能在一个同步方法或同步块中调用 |
| wait() | 导致线程进入等待状态直到它被通知 | 只能在一个同步方法中调用 |
当执行方法wait()之后,锁将自动被释放,但是执行完notify()方法后,锁不会自动释放,必须在执行完notify()方法所在的synchronized代码块之后才释放。
当线程处于等待状态的时候,对线程对象调用interrupt方法会出现InterruptException异常。
在很多情况下,主线程会发起子线程,如果子线程需要进行耗时运算,那么主线程很有可能在子线程执行完成之前结束,但是往往主线程会需要子线程的运行结果,即主线程要等待子线程执行完成之后才能结束,这个时候就需要join()方法了。和wait方法类似,如果方法带有时间参数,那么线程会在时间到期的时候返回。
另外,和sleep()方法不同,join方法和wait方法都会释放锁。
/**
* Waits at most <code>millis</code> milliseconds for this thread to
* die. A timeout of <code>0</code> means to wait forever.
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}当millis==0时,会进入while(isAlive())循环,只要子线程是活的,调用join()方法的线程就会进入wait状态。
