文章内容:
一.为什么需要多线程
二.线程的创建
三.线程的方法sleep(),run(),wait(),yeid(),join(),interrupt()等方法归纳总结
四.线程的状态及其转换
五.线程的交替执行案例
六.多个线程依次执行案例
七.多线程并发带来的线程安全问题
一.为什么需要多线程?
- 相比与以前的单核时代,目前的计算机都是多CPU的,多线程可以更好的利用CPU,提高CPU的利用率
- 在IO密集时,多线程可以提高在线程IO阻塞时CPU的利用率(一个线程被阻塞不占用CPU的时候,另一个线程就可以利用空闲的CPU)
- 多线程的并发可以提高程序的运行速度(一个任务一个任务依次执行肯定是比多个任务同时进行慢的)
- 当然多线程的并发操作也会伴随着线程安全问题(另写文章总结)
二.线程的创建
从本质上说创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。
而要想实现线程执行的内容,却有三种方式:
- 通过实现 Runnable 接口的方式重写 run() 方法
- 继承 Thread 类重写 run() 方法的方式
- 实现Callable并重写call()方法,利用Callable构造一个FutureTask<>,最后把FutureTask当作参数来构造Thread对象,call()方法的内容就是线程执行的内容 (call方法有返回值,而run方法无返回值)
实现 Runnable 接口:
public static class MyRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 通过实现Runable的方式");
}
}
new Thread(new MyRunable()).start();
new Thread(new MyRunable()).run();
从这里也可以看出run方式和start方式的执行过程是不一致的,直接run是在当前run的线程中执行,而start是重新开辟一条新线程执行(后面会总结)
继承 Thread 类重写 run() 方法的方式:
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式");
}
}
new MyThread().start();
new MyThread().run();
Tips:
实现 Runnable 接口比继承 Thread 类实现线程要好在哪里:
- 从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明,Runnable传入给Thread执行就好
- 在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程。如果使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
对于线程池的使用总结,可以点击JUC总结系列篇 (一):对Java线程池的理解和使用总结 - Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,限制了代码未来的可拓展性。
实现Callable并重写call()方法
public static class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + " 通过实现Callable的方式");
return "Call方法执行完毕返回值";
}
}
第一种使用:无需获取返回值
new Thread(new FutureTask<>(new MyCallable())).start();
new Thread(new FutureTask<>(new MyCallable())).run();
第二种使用:获取线程的返回值,通过Future的get方法
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(task);
t1.start();
try {
t1.join();
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
注意上面必须使用join方法,否则main函数可能先执行完了,就获取不到返回值了。
比如:
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(task);
t1.start();
if(!t1.isAlive()){
try {
System.out.println(task.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}else{
System.out.println("t1线程还在执行中,拿不到返回值");
}
关于join方法,后面会讲解
#
三. 线程的sleep(),run(),wait(),yeid(),join(),interrupt()等方法归纳总结
sleep():
public static native void sleep(long millis) throws InterruptedException;
- 静态方法,参数是毫秒,需要指定等待的时间,可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态(注意是在哪个线程里面调用,就使哪个线程进入休眠阻塞)
- 相当于让线程睡眠,交出CPU,让CPU去执行其他的任务,但是要注意,sleep方法是不会释放锁的(does not lose owner ship of any monitors),抱着锁睡觉,也就是说如果有synchronized同步块,线程获取到锁调用sleep后,其他线程仍然不能访问共享数据(因为拿不到锁)
- 执行该方法和线程优先级无关,可以让其他同优先级或者高/低优先级的线程得到执行的机会
使用:
public static class MyRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程进入休眠" + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " 线程结束休眠" + System.currentTimeMillis());
}
}
new Thread(new MyRunable()).start();
- Tips:
操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源比较多,也就是说CPU优先执行优先级高的线程的概率高,但是不能保证优先级高就一定会先被执行。
优先级一共分为1~10个等级,数字越大优先级越高,默认5,超出范围则抛出java.lang.IllegalArgumentException异常。
可以使用 getPriority获取线程的优先级setPriority设置线程的优先级
举个栗子:
public static class MyHighPriorityRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 高优先级输出");
}
}
public static class MyLowPriorityRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 低优先级输出");
}
}
public static void main(String[] args) {
Thread hPThread = new Thread(new MyHighPriorityRunable());
hPThread.setPriority(10);
Thread lPThread = new Thread(new MyLowPriorityRunable());
lPThread.setPriority(1);
lPThread.start();
hPThread.start();
}
改变优先级依旧随机输出:
Thread hPThread = new Thread(new MyHighPriorityRunable());
hPThread.setPriority(1);
Thread lPThread = new Thread(new MyLowPriorityRunable());
lPThread.setPriority(10);
lPThread.start();
hPThread.start();
根据例子可以看出,设置优先级也不能保证优先级高的先输出
执行线程时调用run()和start()辨析:
new Thread(new MyRunable()).start();
new Thread(new MyRunable()).run();
为什么不能直接调用 run() 方法,而需要调用 start() 方法?
- 调用 start() 方法可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行,还是在原来的线程里执行run方法
- 直接执行 run() 方法,会把 run() 方法当成一个 当前线程下的普通方法去执行,并不会开启新线程去执行它,所以这并不是多线程工作。
- start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
public static class MyRunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run方法里开启的线程");
}
}).run();
}
}
new Thread(new MyRunable()).start();
若执行线程的方法改成
new Thread(new MyRunable()).run();
以上就是t.run()和t.start()的区别
yield():
public static native void yield();
- 静态方法
- 调用yield方法会让当前线程交出CPU权限`,让CPU去执行其他的线程,它跟sleep方法类似,同样不会释放锁,但是yield不能控制具体的交出CPU的时间,
- 注意,调用yield方法并不会让线程进入到阻塞状态,而是让线程重新回到就绪状态,等待重新获得CPU执行时间的机会,这一点和sleep的不一样的。
- yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
很少使用,因此不写案例。
wait():
- 这个其实是Object的方法
- 一般需要和notify()以及notifyAll()一起使用,用于协调多个线程对共享数据的存取,必须synchronized语句块内使用,持有拥有对象的锁
- 该方法会释放对象的“锁标志”。当调用后会使当前线程暂停执行(进入等待阻塞状态),并将当前线程放入对象等待池中,直到调用了 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。
notify()/notifyall():
- notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
- notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
案例:利用synchronized + wait + notify使线程交替进行
先从字母开始,然后字母,数字线程交替执行
Object o = new Object();
char[] chs1 = "12345".toCharArray();
char[] chs2 = "ABCDEF".toCharArray();
int len1 = chs1.length;
int len2 = chs2.length;
CountDownLatch latch = new CountDownLatch(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o) {
int i = 0;
for (char num : chs1) {
i++;
System.out.print(num);
try {
if(i < len2){
//数字线程后执行,需要i<len2,
// 因为当i == len2时,字母线程已经执行完了,不能再唤醒数字线程,会造成线程阻塞
o.notify();
o.wait();
}else{
o.notify();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
});
Thread t2 = new Thread(()-> {
synchronized (o) {
int i = 0;
for (char num : chs2) {
try {
i++;
System.out.print(num);
latch.countDown();
if(i <= len1){
//字母线程先执行,所以需要 i<= len1,
// 因为i==len1的时候,数字线程还没有打印完,还可以唤醒字母线程
o.notify();
o.wait();
}else{
o.notify();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t1.start();
t2.start();
该案例用到了synchronized和CountDownLatch保证同步和按顺序执行,属于并发编程的同步解决方案,后面会另写文章总结
读者看不懂没关系,主要记住多线程交替工作的思路是 抢锁–>执行–>让锁就可以了,synchronized就是一把 “锁”
join()方法:
- 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行。
- 如在main主线程当中,调用了thread.join()方法,则main方法会等待thread线程执行完毕或者等待一定的时间,如果调用的是无参join方法,则等待执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。
- join(mills): 传入参数>0则等待线程多少毫秒后并发执行,传入参数若是 <0 则报错,0则等待无限长,直到线程执行完毕,(在A线程里调用B.join(10),那A线程就要等B线程10毫秒后俩者再并发执行)
经典案例:实现十个线程依次计算,最后打印出总和,第1个线程计算1+2+…+10,第2个线程计算11+12+…+20,以此类推
public class CalculateThread extends Thread {
private int stratNum;
public static int sum;//10个线程和
public CalculateThread(int startNum) {
this.stratNum = startNum;
}
public static synchronized void add(int value) {
sum = sum + value;
}
public void run() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum = sum + stratNum + i;
}
System.out.println(Thread.currentThread().getName()+" 计算后和为"+sum);
add(sum);
}
public static void main(String[] args) throws Exception {
Thread[] threadList = new Thread[10];//线程数组
for (int i = 0; i < 10; i++) {
threadList[i] = new CalculateThread(10 * i + 1);
threadList[i].start();
threadList[i].join();//每个线程1开始,就排好join 执行完后下一个线程才能执行
}
System.out.println("10个线程计算结果相加后和为: " + sum);
}
}
Runable和Thread中的run()方法(区别去上面提到的执行时调用的run方法):
- run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,线程等待获得CPU执行时间,一旦获得了CPU执行时间,便进入run方法区执行具体的任务
interrupt():
- 顾名思义,就是中断的意思,单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就是说它可以用来中断一个正处于阻塞状态的线程。
- 根据描述,线程在调用wait和sleep后也是处于阻塞状态的,因此是可以响应中断的
- 注意,调用run方法线程不会处于阻塞状态,所以在run方法中写while(true) 也不会响应中断,不过可以搭配isInterrupted()中断线程
- Thread.interrupted()是一个静态方法,返回中断值,默认是false,被中断后是true
它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位(又置为false)。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
案例
- Thread.interrupted()
while (true){
if(isInterrupted()){
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
return;
}
}
MyThread t1 = new MyThread();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
可知在调用Thread.interrupted()后,中断标志位又变为false了
- 利用isInterrupted() + interrupt()来中断 run方法中的while(true)
public static class MyThread extends Thread{
@Override
public void run() {
while(true){
if(this.isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " " + Thread.interrupted());
return;
}
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式 " + Thread.interrupted());
}
}
}
MyThread t1 = new MyThread();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
- 中断sleep()
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式 " + Thread.interrupted());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式 " + Thread.interrupted());
}
}
MyThread t1 = new MyThread();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
- 中断wait()
public static class MyThread extends Thread{
@Override
public void run() {
Object o = new Object();
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式 " + Thread.interrupted());
try {
synchronized (o){
o.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " 通过继承Thread的方式 " + Thread.interrupted());
}
}
MyThread t1 = new MyThread();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
wait和sleep的区别和联系
- sleep() 不会释放锁,wait() 会释放锁。
- 都可以暂停线程的执行
- wait()通常用来进行线程间的交互/通信,通过Notify() 或 NotifyAll()唤醒,而sleep()用来暂停执行
- wait() 方法被调用后,不会自动苏醒。需要别的线程来调用同一个锁对象上的 notify/notifyAll 方法。sleep() 方法调用后,经过设置后的时间后会苏醒,也可以通过调用 wait(long timeout),超时后也能自动苏醒。
上述中断wait案例中:若把wait设置个时间,比sleep少,那么线程就赶在被中断前苏醒,就不会被中断了
o.wait(500);
Thread.sleep(2000);
四.线程的状态以及转换
- NEW:
初始/新建状态,线程被创建了,但还没有执行start()方法;
Thread state for a thread which has not yet started. - RUNNABLE(就绪状态和运行状态统称):
运行状态,这里对应操作系统中的就绪和运行状态,统称为运行中
Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.(在JVM是运行状态,但在操作系统的其他程序可能是等待中(就绪)) - BLOCKED:
阻塞状态,表示线程因为锁被阻塞了
Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter(重入) a synchronized block/method after calling Object.wait.
在等待锁去进入一个synchronized修饰的代码块或方法时,以及调用了wait方法(把锁给让出去)后再等待锁去进入一个synchronized修饰的代码块或方法时会处于这个状态 - WAITING
等待状态,线程进入等待状态,需要等待其他线程通过 通知 中断 来唤醒
Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
Object.wait with no timeout
Thread.join with no timeout
LockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.
以下时候线程会处于这个状态:
1.Object.wait(),不传入参数
2.Thread.join(),不传入参数
注意上面是对象调用,因为wait和join都不是静态方法
3.LockSupport.park()
4.举例子,调用wait后等待notify/notifyall唤醒时,以及等待join进来的线程执行完后等 - TIME_WAITING:
超时等待状态,不同于WAITING,可以在指定时间内自动返回
Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
Thread.sleep
Object.wait with timeout
Thread.join with timeout
LockSupport.parkNanos
LockSupport.parkUntil
以下时候线程会处于这个状态:
1.Object.wait(mills),传入参数
2.Thread.join(mills),传入参数
注意上面是对象调用,因为wait和join都不是静态方法
3.LockSupport.parkNanos
4.LockSupport.parkUntil
5.Thread.sleep() - TERMINATED:
终止状态,表示线程已经执行完毕
Thread state for a terminated thread. The thread has completed execution.
由于每个人的翻译不一样,看的教材也不一样,所以对线程状态的描述也不太一致,其他博客可能也有不一样的称呼。 - 状态转移图:
五.线程交替执行案例----翻上去
六.多线程依次执行案例 ----翻上去
七.多线程并发带来的安全性问题
并发安全性问题的根源:
- 原子性 : 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。(经典转账问题,一个扣钱一个加钱,必须保证原子性) synchronized可以保证代码片段的原子性
- 可见性 : 当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile和synchronized关键字都可以保证共享变量的可见性
- 有序性 : 代码在执行的过程中的先后顺序,JAVA在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序,volatile关键字可以禁止指令进行重排序优化。
在计算机内部,CPU,内存,I/O设备的速度有着极大的差异,CPU巨快,为了合理充分利用CPU的高性能,平衡这三者的速度差异,计算机体系结构,操作系统,编译程序 都做出了贡献
-
通过设置缓存(计算机体系结构):将一些关键的,常用的数据放置在CPU缓存中,以提高数据的获取速度,均衡了CPU的快速而磁盘读取缓慢的速度差异。但设置缓存也导致了 可见性 的问题。
-
操作系统增加了进程,线程,以分时复用CPU,进而充分利用CPU,均衡CPU和I/O设备的速度差异,在进行线程切换的时候容易导致了原子性问题
-
编译程序优化指令执行次序,使得缓存能够得到更加合理的利用(局部性原理),但这也带来有序性的问题,代码的实际执行顺序与代码书写顺序并不一致
解决多线程并发带来的安全性问题的本质其实就是解决原子性,可见性,有序性 带来的问题
由于这是属于多线程并发的内容,因此会另起文章总结。
如volatile,以及锁机制Synchronized和Lock等的原理和使用届时都会归纳总结。
参考与感谢
由于这是很久之前学习的知识做的本地笔记,现今我重新实践并且做归纳总结,因此不知道具体参考了哪些教程和博客。所以我在此感谢所有参考到的教材作者以及博客作者,创造不易。也感谢在这个计数开源的时代,让我能找到很多优秀教程进行学习,并且输出。
真心推荐这篇文章可以让初学者进行归纳学习,毕竟是站在巨人的肩膀上总结的精华。喜欢的可以点赞加关注