一、多线程的创建
1、继承Thread类,重写run()方法:
①、定义类继承Thread;
②、复写T=hread类中的run方法;
目的:将自定义代码存储在run方法,让线程运行
③、调用线程的start方法:
该方法有两步:启动线程,调用run方法。
2、实现Runnable接口,实现接口run()方法:
接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。
①、定义类实现Runnable接口
②、覆盖Runnable接口中的run方法
将线程要运行的代码放在该run方法中。
③、通过Thread类建立线程对象。
④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象
⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
3、实现Callable接口,实现call()方法:
①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
三种方式区别
主要是单继承局限性、有无返回值、可否抛出异常3点上
(1)继承Thread:线程代码存放在Thread子类run方法中。
优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
劣势:已经继承了Thread类,无法再继承其他类。
(2)实现Runnable:线程代码存放在接口的子类的run方法中。
优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
(3)实现Callable:
优势:有返回值(用来判断线程是否已经执行完毕或者取消线程执行)、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法
(4)异常
Callable()的call()方法可以抛出异常(在调用类中用try/catch捕获),而Runnable()的run()方法不可以
建议使用实现接口的方式创建多线程。
二、线程的生命周期(状态)
- 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
- 就绪 :线程对象调用
start()
方法后,就处于就绪状态,等到JVM里的线程调度器的调度; - 运行 :就绪状态下的线程在获取CPU资源后就可以执行
run()
,此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡( stop() )三种状态。 - 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
- 终止 :
run()
方法完成后或发生其他终止条件时就会切换到终止状态。
线程状态的变更
(1)新建New状态
- 通过start()进入Runnable状态
(2)就绪Runnable状态
- 获取CPU资源后进入Running
Runnable状态的线程无法直接进入Blocked状态和Terminated状态的。只有处在Running状态的线程,换句话说,只有获得CPU调度执行权的线程才有资格进入Blocked状态和Terminated状态,Runnable状态的线程要么能被转换成Running状态,要么被意外终止。
(3)运行Running状态
- 被转换成Terminated状态,比如调用 stop() 方法;
- 被转换成Blocked状态,比如调用了sleep, wait 方法被加入 waitSet 中;
- 被转换成Blocked状态,如进行 IO 阻塞操作,如查询数据库进入阻塞状态;
- 被转换成Blocked状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;
- 该线程的时间片用完,CPU 再次调度,进入Runnable状态;
- 线程主动调用 yield 方法,让出 CPU 资源,进入Runnable状态
(4)阻塞Blocked状态
- 被转换成Terminated状态,比如调用 stop() 方法,或者是 JVM 意外 Crash;
- 被转换成Runnable状态,阻塞时间结束,比如读取到了数据库的数据后;
- 完成了指定时间的休眠,进入到Runnable状态;
- 正在wait中的线程,被其他线程调用notify/notifyAll方法唤醒,进入到Runnable状态;
- 线程获取到了想要的锁资源,进入Runnable状态;
- 线程在阻塞状态下被打断,如其他线程调用了interrupt方法,进入到Runnable状态;
(5)终止Terminated状态
一旦线程进入了Terminated状态,就意味着这个线程生命的终结,哪些情况下,线程会进入到Terminated状态呢?
- 线程正常运行结束,生命周期结束;
- 线程运行过程中出现意外错误;
- JVM 异常结束,所有的线程生命周期均被结束。
三、多线程同步的实现方法
1、synchronized方法
在方法声明前加入synchronized关键字
public synchronized void Test1(){}; //锁对象是类的实例对象
public synchronized static void Test2(){}; //锁对象是类本身(.class文件)
2、同步代码块
灵活性高
synchronized(锁对象){...} //锁对象一般是当前类的实例化对象
3、Lock锁
创建Lock接口的实现类ReentrantLock(重入锁)对象,将会出现线程安全的代码用lock()
和unlock()
包裹
ReentrantLock lock = new ReentrantLock();
lock.lock(); //获取锁
//lock.tryLock(); 以非阻塞方式尝试获取锁,获取到返回true,否则返回false
...
lock.unlock(); //释放锁
区别
(1)性能不一样
在资源竞争不激烈的情况下,synchronized
优于ReentrantLock
在资源竞争激烈的情况下,synchronized
性能下降的非常快,而ReentrantLock
基本保持不变
(2)锁的机制不一样
synchronized
:自动解锁,不会因为异常,导致锁没有释放,而出现死锁
ReentrantLock
:需要开发人员手动释放锁,并且必须在finally
块中释放,否则会引起死锁。还具有tryLock()方法,能以非阻塞方式尝试获取锁。
四、结束线程的方法
(1)程序正常运行结束
(2)在一些需要长时间运行的线程中,可用一个变量来控制循环,当变量满足某个条件,退出循环。变量定义时使用了一个 Java 关键字 volatile,这个关键字的目的是使 变量同步,也就是说在同一时刻只 能由一个线程来修改变量的值。
(3)stop()方法,线程不安全,调用 thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈 现不一致性
(4)interrupt()方法:
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实 际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正 常结束run方法。
线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
五、获取所有线程、中断指定线程
Thread thread = new Thread(...);
thread.setName("heartBeat");
thread.start();
// 获取当前线程所在的线程组
ThreadGroup group = Thread.currentThread().getThreadGroup();
// 获取所有线程
while(group != null) {
Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
int count = group.enumerate(threads, true);
for(int i = 0; i < count; i++) {
// 查找指定线程
if("heartBeat".equals(threads[i].getName()) && threads[i].isAlive()) {
// 中断线程
threads[i].interrupt();
return "断开成功";
}
}
// 获取该线程组的父线程组
group = group.getParent();
}
六、sleep()和wait()区别
//线程休眠1秒
Thread.sleep(1000);
TimeUnit.SECONDS.sleep(1); //企业常用
(1)来自不同的类
sleep()方法属于Thread类中的。而wait()方法,则是属于 Object类中的。
(2)wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
在调用sleep()方法的过程中,线程不会释放对象锁。
sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然 保持着,当指定的时间到了又会自动恢复运行状态。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(进入阻塞状态),只有针对此 对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态(就绪状态)。
(3)使用范围
wait:在同步代码块中
sleep:可以在任意地方
当你在调用notify(), notifyAll(),wait(), wait(long), wait(long, int)等线程控制操作方法时,必须要有两个前提。第一:必须要在被synchronized关键字控制的同步代码块中,才能调用这些方法。第二,调用者必须为你当前的锁对象。
七、start()和run()区别
(1)start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。
(2)通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运 行。
(3) 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运 行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
(4)如果直接调用run()方法,则不会开启多线程,而是以单线程的方式执行run()方法
八、同步代码块对象锁和类锁区别
synchronized (this):对象锁
synchronized (System.class):类锁
当存在多个对象调用同步代码块时,对象锁只针对单个对象的多线程。而类锁可以锁住所有线程、所有对象。
对象锁
public class SynLockTest {
@Test
public void test1() throws IOException {
//创建连个对象
User user1 = new User("张三");
User user2 = new User("李四");
UserThread userThread1 = new UserThread(user1);
UserThread userThread2 = new UserThread(user2);
//2、启动线程
for (int i = 0; i < 10; i++) {
new Thread(userThread1).start();
new Thread(userThread2).start();
}
System.in.read();
}
}
//自定义一个线程类
class UserThread implements Runnable{
private User user;
public UserThread(User user){
this.user = user;
}
@Override
public void run() {
try {
user.info();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class User{
public String username;
public User(String username){
this.username = username;
}
public void info() throws InterruptedException {
synchronized (this){
// System.out.println("this="+this);
Thread.sleep(1000);
System.out.println("当前线程的名字为:"+Thread.currentThread().getName()+"____"+username);
}
}
}
发现结果是两条数据一起打印的,说明当前的锁并没有起作用
类锁
public void info() throws InterruptedException {
synchronized (System.class){
// System.out.println("this="+this);
Thread.sleep(1000);
System.out.println("当前线程的名字为:"+Thread.currentThread().getName()+"____"+username);
}
}
发现数据是一行一行输出的,说明同步代码块中的锁起到了作用
九、suspend()和 resume()方法
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume()使其恢复。
注意区别:
初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。
final Object lock = new Object();
@Test
public void test1() throws InterruptedException {
new Thread(()->{
synchronized (lock){
try {
System.out.println("1-1");
lock.wait();
System.out.println("1-2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
synchronized (lock){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2-1");
lock.notifyAll();
System.out.println("2-2");
}
}).start();
TimeUnit.SECONDS.sleep(5);
}
suspend()
和 resume()
final Object lock = new Object();
final Object lock2 = new Object();
@Test
public void test2() throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("1-1");
Thread.currentThread().suspend();
System.out.println("1-2");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread1.start();
new Thread(()->{
// 如果这里采用 lock, 则会死锁, 因为 suspend() 不会释放锁
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2-1");
thread1.resume();
System.out.println("2-2");
}
}).start();
TimeUnit.SECONDS.sleep(5);
}
两个 @Test
方法的输出
1-1
2-1
2-2
1-2
十、sleep() 和 yield()方法
这两个方法都定义在Thread.java中
**sleep()**的作用是让当前线程休眠(正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务),即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时候会大于或者等于该休眠时间,当时间过后该线程重新被会形式,他会由“阻塞状态”编程“就绪状态”,从而等待cpu的调度执行,注意:sleep方法只是让出了cpu的执行权,并不会释放同步资源锁。
**yield()**的作用是让步,它能够让当前线程从“运行状态”进入到“就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权,也有可能是当前线程又回到“运行状态”继续运行,注意:这里我将上面的“具有相同优先级”的线程直接改为了线程,很多资料都写的是让具有相同优先级的线程开始竞争,但其实不是这样的,优先级低的线程在拿到cpu执行权后也是可以执行,只不过优先级高的线程拿到cpu执行权的概率比较大而已,并不是一定能拿到。
注意:无法使用 yield()
唤醒调用 wait()
的线程
举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,
有可能是其他人先上车了,也有可能是Yield先上车了。
但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,
最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。