Java多线程超详解
1.创建线程有哪几种方式?
继承Thread类、实现Runnable接口、实现Callable接口
class MyThread2 extends Thread{
@Override
public void run() {//run()方法被称为线程执行体
for(int i = 0; i < 100; i++){
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName() + i);
}
}
}
}//1.定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
//2.创建Thread子类的实例,即创建了线程对象。
MyThread1 t1 = new MyThread1("线程一");
//3.调用线程对象的start()方法来启动该线程。
t1.start();//①启动线程 ②调用当前线程的run(),注意不能直接执行run方法,直接执行还没启动线程
//只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
优先采用实现Runnable、Callable接口的方式创建多线程
-
继承了Thread类,就不能继承其他类了,不满足实际的需求。
-
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类,并且多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况
2.Thread类的常用方法
1.start(开启线程,start是通过线程来调用run方法)
2.run 此run非彼run (不是在run方法实现线程的逻辑,而是thread.run(),这个run方法是直接调用了线程中的run)
3.yield(暂停当前线程,并执行其他线程)
4.sleep(使当前线程由运行状态变成阻塞状态,若睡眠时其他线程调用了interrupt方法,会导致sleep抛出异常InterruptException)
5.join(保证当前线程在其他线程开始时会结束)(如下,A线程想运行的话,必须等B线程结束才能运行(将处于阻塞状态))
Thread A{undefined
run{undefined
new ThreadB.join();
}
}.start;
6.interrupt(中断线程)
7.wait/notify(从Object类继承下来的方法)
8.setPriority(设置线程优先级(只能在线程开始前设置))
9.stop(强制结束线程)
3.介绍一下线程的生命周期
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)
5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
new关键字创建线程,该线程就处于新建
状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪
状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。
- 当发生如下情况时,线程将会进入阻塞状态:
线程调用sleep()方法
主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法
,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步监视器
,但该同步监视器正被其他线程所持有。
线程在等待某个通知(notify
)。
程序调用了线程的suspend()
方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。 - 针对上面几种情况,可以解除上面的阻塞,让该线程重新进入就绪状态:
调用sleep()方法的线程经过了指定时间。
线程调用的阻塞式IO方法已经返回。
线程成功地获得了试图取得的同步监视器。
线程正在等待某个通知时,其他线程发出了一个通知。
处于挂起状态的线程被调用了resume()恢复方法。 - 线程会以如下三种方式结束,结束后就处于死亡状态:
run()或call()方法
执行完成,线程正常结束。
线程抛出一个未捕获的Exception或Error
。
直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。
4.如何实现线程同步?
- 同步方法
即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。**在调用该方法前,需要获得内置锁,否则就处于阻塞状态。**需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。 - 同步代码块
即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 - ReentrantLock
ReentrantLock 锁详解 - volatile
volatile关键字是由JVM提供的最轻量级同步机制。
彻底理解volatile关键字 - 原子变量
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
原子操作类AtomicInteger详解
5.Java多线程之间的通信方式
sleep()和wait()的区别
notify()、notifyAll()的区别
例题:线程A负责实时往Map里put 1-100的数字,线程B负责实时从这个map中get数字
并进行累加(A放入MAP一个值后,B取出来,然后A继续放,B继续取,以此循环一直到A放完100,B取完100,结束),B实时打印当前时刻累加的结果。
public class WindowTest1 {
public static void main(String[] args) {
Window1 window1 = new Window1();
Thread t1 = new Thread(window1);
Thread t2 = new Thread(window1);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
class Window1 implements Runnable{
HashMap<Integer, Integer> map = new HashMap<>();
private static int ticket = 1;
private static boolean flag = true;
private int sum = 0;
@Override
public void run() {
if(flag){
flag = false;
show1();
}else{
flag = true;
show2();
}
}
public synchronized void show1(){
while(ticket <= 100) {//使用 while而不是if
map.put(ticket, ticket);
System.out.println(Thread.currentThread().getName() + ": " + ticket);
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
} //同步函数的锁是this
}
}
public synchronized void show2(){
while(ticket <= 100){
sum += map.get(ticket);
ticket++;
System.out.println(Thread.currentThread().getName() + ": " + (ticket-1));
System.out.println("累加结果: " + sum);
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
} //同步函数的锁是this
}
}
}
6.进程和线程
根本区别:进程是操作系统进行资源分配的最小单元,线程是操作系统进行运算调度的最小单元。
从属关系不同:进程中包含了线程,线程属于进程。