参考:
https://www.runoob.com/java/java-multithreading.html
https://www.cnblogs.com/qingyunzong/p/8270271.html
部分内容按自己理解加粗
线程的声明周期
- 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running) : 线程获取CPU权限进行执行run()。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的实现
一、实现Runnable接口
Runnable 是一个函数式接口,该接口中只包含了一个run()方法。它的定义如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。
class MyThread implements Runnable{
private int ticket=10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票:ticket"+this.ticket--);
}
}
}
};
public class RunnableTest {
public static void main(String[] args) {
MyThread mt=new MyThread();
// 启动3个线程t1,t2,t3(它们共用一个Runnable对象),这3个线程一共卖10张票!
Thread t1=new Thread(mt);
Thread t2=new Thread(mt);
Thread t3=new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
1 Thread-0 卖票:ticket10
2 Thread-2 卖票:ticket8
3 Thread-1 卖票:ticket9
4 Thread-2 卖票:ticket6
5 Thread-0 卖票:ticket7
6 Thread-2 卖票:ticket4
7 Thread-1 卖票:ticket5
8 Thread-2 卖票:ticket2
9 Thread-0 卖票:ticket3
10 Thread-1 卖票:ticket1
二、继承Thread类
Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:
public class Thread implements Runnable {}
Thread的作用,实现多线程。
class MyThread extends Thread{
private int ticket=10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(this.getName()+" 卖票:ticket"+this.ticket--);
}
}
}
};
public class ThreadTest {
public static void main(String[] args) {
// 启动3个线程t1,t2,t3;每个线程各卖10张票!
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
1 Thread-0 卖票:ticket10
2 Thread-1 卖票:ticket10
3 Thread-2 卖票:ticket10
4 Thread-1 卖票:ticket9
5 Thread-0 卖票:ticket9
6 Thread-1 卖票:ticket8
7 Thread-2 卖票:ticket9
8 Thread-1 卖票:ticket7
9 Thread-0 卖票:ticket8
10 Thread-1 卖票:ticket6
11 Thread-2 卖票:ticket8
12 Thread-1 卖票:ticket5
13 Thread-0 卖票:ticket7
14 Thread-1 卖票:ticket4
15 Thread-2 卖票:ticket7
16 Thread-1 卖票:ticket3
17 Thread-0 卖票:ticket6
18 Thread-1 卖票:ticket2
19 Thread-2 卖票:ticket6
20 Thread-2 卖票:ticket5
21 Thread-2 卖票:ticket4
22 Thread-1 卖票:ticket1
23 Thread-0 卖票:ticket5
24 Thread-2 卖票:ticket3
25 Thread-0 卖票:ticket4
26 Thread-2 卖票:ticket2
27 Thread-0 卖票:ticket3
28 Thread-2 卖票:ticket1
29 Thread-0 卖票:ticket2
30 Thread-0 卖票:ticket1
Thread和Runnable的异同点
- Thread 和 Runnable 的相同点:
都是“多线程的实现方式”。 - Thread 和 Runnable 的不同点:
Thread 是类,而Runnable是接口;
Thread本身是实现了Runnable接口的类。
我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
通常,建议通过“Runnable”实现多线程!
start() 和 run()的区别说明
- start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
- run() : run()就和普通的成员方法一样,可以被重复调用。
单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!
例如
class MyThread extends Thread{
public void run(){
...
}
};
MyThread mythread = new MyThread();
- mythread.start()会启动一个新线程,并在新线程中运行run()方法。
- mythread.run()会直接在当前线程中运行run()方法,并不会启动一个新线程来运行run()。
start() 和 run()的区别示例
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
public void run(){
System.out.println(Thread.currentThread().getName()+" is running");
}
};
public class Demo {
public static void main(String[] args) {
Thread mythread=new MyThread("mythread");
System.out.println(Thread.currentThread().getName()+" call mythread.run()");
mythread.run();
System.out.println(Thread.currentThread().getName()+" call mythread.start()");
mythread.start();
}
}
运行结果
main call mythread.run()
main is running
main call mythread.start()
mythread is running
结果说明:
(01) Thread.currentThread().getName()是用于获取“当前线程”的名字。当前线程是指正在cpu中调度执行的线程。
(02) mythread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。
(03) mythread.start()会启动“线程mythread”,“线程mythread”启动之后,会调用run()方法;此时的run()方法是运行在“线程mythread”上。
线程的优先级
一、线程的优先级及设置
线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级高的线程将优先执行。
一个线程的优先级设置遵从以下原则:
- 线程创建时,子继承父的优先级。
- 线程创建后,可通过调用setPriority()方法改变优先级。
- 线程的优先级是1-10之间的正整数。
1- MIN_PRIORITY
10-MAX_PRIORITY
5-NORM_PRIORITY
如果什么都没有设置,默认值是5。 - 不能依靠线程的优先级来决定线程的执行顺序。
class MyThread1 extends Thread{
MyThread1(String name){
super(name);
}
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class TestThread4 {
public static void main(String[] args) {
// 线程优先级
// 1- 10优先级
//1 最低,10最高
MyThread1 t1 = new MyThread1("t1");
MyThread1 t2 = new MyThread1("t2");
//
/* t1.setPriority(1);
t2.setPriority(10);*/
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
二、线程的调度策略
线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:
- 线程体中调用了yield()方法,让出了对CPU的占用权。
- 线程体中调用了sleep()方法,使线程进入睡眠状态。
- 线程由于I/O操作而受阻塞。
- 另一个更高优先级的线程出现。
- 在支持时间片的系统中,该线程的时间片用完。
线程管理
一、线程睡眠——sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
注:
- sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName());
MyThread myThread=new MyThread();
myThread.start();
myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
}
- Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
二、线程让步——yield
yield()方法和sleep()方法有点相似,
- 它也是Thread类提供的一个静态的方法;
- 它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。
但是和sleep()方法不同的是,
- 它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。用法如下:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
this.setPriority(pro);// 设置优先级
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
if (i % 5 == 0)
Thread.yield();
}
}
}
关于sleep()方法和yield()方的区别
- sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。
yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。 - sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。
yield方法则没有声明抛出任务异常。 - sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
三、线程合并——join
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,
应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,
Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
它有3个重载的方法:
void join()
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
@TODO 示例
四、后台(守护)线程
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。
还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程的用途为:
- 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
- Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
setDaemon方法的详细说明:
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数:
on - 如果为 true,则将该线程标记为守护线程。
抛出:
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。
注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态。
因此,在使用后台线程时候一定要注意这个问题。
五、正确结束线程
Thread.stop()、Thread.suspend()、Thread.resume()、Runtime.runFinalizersOnExit()这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!
想要安全有效的结束一个线程,可以使用下面的方法:
- 正常执行完run()方法,然后结束掉;
- 控制循环条件和判断条件的标识符来结束掉线程。
class MyThread extends Thread {
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
System.out.println(i);
}
}
}
线程的同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。
例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
不同线程对同步锁的访问是互斥的。
也就是说,某时间点,对象的同步锁只能被一个线程获取到!
通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。
例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。
假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;
而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。
一、同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。
在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
synchronized public void setMoney() {
money += 100;
System.out.println(Thread.currentThread().getName()+":"+money);
}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
二、同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public class Bank {
private int count =0;//账户余额
//存钱
public void addMoney(int money){
synchronized (this) {
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
public void subMoney(int money){
synchronized (this) {
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
public void checkMoney(){
System.out.println("账户余额:"+count);
}
}
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
同步锁释放的情况
- 同步块或同步方法中的代码正常执行完了。
- break;return会结束同步块或同步方法。
- 同步块或同步方法中有没有被捕获的异常
- 当执行了wait方法时
三、使用重入锁(Lock)实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
public void getTicket() {
try {
lock.lock();//加锁
count--;
System.out.println(Thread.currentThread().getName()+"卖出1张票,剩余"+count+"张。");
return;
}finally {
lock.unlock();//释放锁
}
}
每个对象只有一个锁(lock)与之关联。
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
四、死锁
死锁发生在当多个线程进入到了循环等待状态。
死锁是很难调试的错误。
通常,它极少发生,只有到两线程的时间段刚好符合时才能发生。
我们在编写多线程并含有同步方法调用的程序中要格外小心,避免死锁的发生。