1. Java线程理解
进程:进程就相当于一个应用程序,而线程是进程中的执行场景或者说执行单元,一个进程可以启动多个线程。
线程并发:对于电脑的CPU,例如4核的CPU,表示在同一个时间点上,可以真正做到有4个进程并发执行。而对于单核CPU,是不能做到真正的多线程并发的,只是由于CPU在线程之间切换太快,让我们人在使用时产生了多个线程在同时运行的假象,在主观感觉上多个线程是并发的,但其实单核的CPU是不能做到真正的并发的。
JVM进程:运行Java程序,首先会先启动一个JVM,JVM就是一个进程,然后JVM再启动一个主线程调用main方法,与此同时,再启动一个垃圾回收线程负责看护main主线程并回收其产生的垃圾。所以,一个Java程序中至少会有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
线程的内存使用:Java中堆内存和方法区内存在线程间是共享的,也就是它们在程序运行期间都只有“一块”,但是栈是独立的,每一个线程拥有一个自己的栈,启动了多少线程就会有多少块栈内存。
2. 创建线程的三种方式:Thread,Runnable,Callable
Thread方式:定义一个类,继承java.lang.Thread,并重写run方法即可。运行时,调用线程对象的start方法,然后JVM就会自动创建一个分支线程(分支栈)来运行run方法中的代码。这种方式也是最核心的,其他两种方式都是基于这个Thread来实现的。
Thread中的常用方法:
- void start():start()方法的作用是启动一个分支线程,调用时会在JVM中开辟出一个新的栈空间,这个栈空间开辟出来后start()方法就结束了,表示线程启动成功了。注意,start()方法本身并不属于新的分支线程,而是属于调用者线程。start()方法结束后,启动成功的线程会自动调用run方法,并且run方法处于分支栈的底部(压栈),其作用和意义就相当于是分支栈的main方法,即主线程的main方法和分支线程的run方法对于各自的线程来讲是意义一样的。
- void run():如果在当前线程中直接调用run方法,那它就是线程对象中的一个普通方法,并不会启动一个新的分支线程,所以想要启动一个新的分支线程,必须要通过调用start方法来运行run方法中的代码。
- String getName():获取线程的名称,默认为“Thread-[n]”,n表示数字。
- void setName(String name):设置线程的名称。
- static Thread currentThread():获取当前线程的线程对象,当前线程指的是正在执行currentThread()这个方法的线程。(注意这是个静态方法)
- static void sleep(long millis):使当前线程暂停执行指定毫秒数。(注意这是个静态方法)
- void interrupt():中断sleep的睡眠。原理是调用sleep方法进行睡眠时,会产生一个InterruptedException的编译时异常,代码中通常会使用try块将sleep方法包裹起来,当调用interrupt方法时,就会主动抛出一个InterruptedException异常,此时的sleep睡眠就被中断了。
- static void yield():线程让位,让当前线程短暂的暂停一下,以便让其他线程得以有更多时间执行。(注意这是个静态方法)
- void join():线程合并,让当前线程阻塞,直到调用join方法的线程执行完毕,即让其他线程合入当前线程。
- void setDaemon(boolean on):将on设置true传入,表示在线程调用start之前将其设置为守护线程,注意,这个方法需要在线程启动之前调用进行设置。Java中线程分为两类,用户线程和守护线程,守护线程也称为后台线程,而且守护线程通常是一个死循环程序,并且所有的用户线程结束之后,守护线程就会自动结束,不用程序员手动去结束。主线程main线程是属于用户线程,而垃圾回收机制的线程则属于守护线程。
Thread简单示例:
public class ThreadTest{ public static void main(String[] args){ // main方法中的代码属于主线程,在主栈中运行 MyThread myThread = new MyThread(); // 调用线程对象的start方法会启动一个新的分支线程,并执行线程对象中run方法的代码 // 此时主线程的main方法并不会等myThread的run方法运行完毕,而是会直接往下继续执行 // 因为它们属于两个独立的线程,它们的运行是并行执行的 myThread.start(); System.out.println("主线程正在运行..."); } } class MyThread extends Thread { public void run(){ // run方法中的代码会运行在创建的分支线程中 System.out.println("分支线程正在执行..."); } }
Runnable方式:定义一个类,实现java.lang.Runnable接口,并重写接口的run方法,这个类也称之为可运行的类。然后再创建一个Thread对象,在创建Thread对象时,构造方法中将这个自定义的可运行类对象传入即可。
注:这种实现接口的方式其实更加常用,因为定义的可运行类在将来还可以继承别的类,但定义Thread子类的方式因为Java只支持单继承的原因就没有机会再继承别的类了,即无法通过继承的方式扩展功能了。
Runnable简单示例:
public class ThreadTest{ public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t = new Thread(myRunnable); // 启动分支线程,并在分支线程中运行myRunnable对象中的run方法 t.start(); System.out.println("主线程正在运行..."); } } // 这只是一个实现了Runnable接口的普通类 // 只有将它传入Thread对象才能在单独的线程中运行 class MyRunnable implements Runnable{ public void run(){ System.out.println("分支线程正在执行..."); } }
Callable方式:实现java.util.concurrent.Callable接口,并重写call()方法,具体使用方法见示例。这种方式的特点是可以获取线程的返回值。但是,也有一个缺点,调用get方法获取返回值时会阻塞当前线程。
Callable简单示例:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws Exception{ // FutureTask使用了泛型,使用时可以传入自己需要的类型 // 这里采用了匿名内部类的实现方式 FutureTask task = new FutureTask(new Callable(){ // 需要重写call方法,就相当于Thread中的run方法 @Override public Object call() throws Exception { System.out.println("Callable线程正在运行..."); return new Object(); } }); Thread t = new Thread(task); t.start(); // 执行get方法是会阻塞当前线程(这里是主线程main), // 直到线程t执行完毕 Object obj = task.get(); System.out.println("线程执行的结果:" + obj); } }
3. 使用布尔标记终止线程
终止线程的方法具体的使用场景可能有所不同,以下示例只是常用方法之一。
// 终止线程的一种方式:定义一个布尔标记 public class ThreadTest{ public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t = new Thread(myRunnable); t.start(); // 主线程暂停5秒 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } // 主线程暂停5秒之后,手动去终止t线程 myRunnable.run = false; } } class MyRunnable implements Runnable{ // 定义一个布尔标记 boolean run = true; public void run(){ // 让当前线程sleep 10秒,模拟程序执行10秒 for (int i = 0; i < 10; i++) { if (run) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 终止线程 return; } } } }
4. 线程生命周期
新建状态:创建线程对象之后,调用start()方法之前,线程就处于新建状态,此时线程还没有被创建,因为一旦创建线程成功之后就会立马进入就绪状态。
就绪状态:调用start()方法启动线程之后,线程就处于就绪状态,就绪状态表示此时的线程拥有抢夺CPU的时间片的权利(CPU执行权),即我这个线程可以占用多少时间的CPU。当抢到时间片之后就会进入运行状态。
运行状态:当线程抢到时间片之后就会去占用CPU,并使用CPU执行run()方法中的代码,一直到这个时间片使用完毕。时间片使用完毕之后,run方法中的代码会暂停执行并进入就绪状态,等下一次再次抢到时间片的时候就会继续运行run方法中的代码了。当在使用时间片的过程中,程序阻塞了,例如需要等待用户输入、程序sleep等,就会立马释放掉拥有的时间片,并进入阻塞状态。
阻塞状态:当进入阻塞状态后,直到用户输入完毕、sleep时间到等,此时会解除程序的阻塞状态,并使程序进入就绪状态,继续参与CPU执行权的抢夺以便运行后续的代码。
注:对于单核CPU来说,主线程和分支线程并发时,它们都在抢夺CPU的时间片,由于它们的运行状态交替太快,导致了我们主观感觉上的并行,但其实并没有真正的并发执行。
5. synchronized关键字
可以使用synchronized语法来实现线程之间的同步,以给某个代码块、方法或者类添加锁的方式,以达到数据安全的目的。
代码块中的synchronized:在代码块中使用synchronized,语法如下:
/* 例如线程t1、t2、t3之间共享对象testShare,而需要同步的代码正好是testShare中的一个方法, 那么synchronized就需要用在testShare中, 小括号中的"线程之间共享的对象"就可以写this,而方法体中的代码就可以放在synchronized 的大括号中来执行。这样,同一个类new出来的不同对象就可以实现各自的线程间同步,互不干扰。 注意:线程之间共享需要共享的对象可能是不同的,而大括号中的代码和共享对象之间不一定是有关系的, 这两个部分可以是没有关系的,所以这里不一定是this。这个共享对象只是给线程获取锁提供了一个对象, 多个线程之间只有需要获取相同对象的锁的时候,才会发生线程的同步。 */ synchronized(线程之间共享的对象){ 需要同步的代码 }
synchronized原理:synchronized语法实现线程之间同步的原理其实就是线程对对象锁的占有和释放,每一个Java对象都有一个锁(其实就是一个标记,我们称之为锁而已),当第一个线程遇到synchronized之后就会占有小括号中“共享对象”的锁,然后执行大括号中需要同步的代码块,如果在执行过程中,第二个线程也来到了这里,遇到了synchronized,也会去占有这个“共享对象”的锁,但是发现它已经被占有了,那么就只好排队等待,直到第一个线程执行完毕,释放这个“共享对象”的锁,然后第二个线程才能占有锁并继续执行后面的代码。以此类推,后面的线程也会来占有锁,如果锁已经被占有了,就停止执行并等待,直到“有锁可占”,如此,也就达到了这段代码的线程间同步。
synchronized效率提升:
- 大括号中的内容越多,范围越大,执行效率越低,所以应该尽量保证大括号中的内容少一点,范围小一点。
- 对于局部变量,因为它始终都在栈中,而各自的线程都有自己的栈,所以局部变量是不存在线程安全问题的,因此,对于Java中的某些引用数据类型,在局部变量的使用中,应该使用非线程安全的数据类型,比如ArrayList、HashSet、StringBuilder等,它们虽然本身不是线程安全的,但是因为是局部变量,所以不存在线程安全问题,也就不用去考虑它们本身的线程安全问题了。
方法定义中的synchronized:synchronized可以在方法定义上使用,此时共享对象默认为this,同步的代码为整个方法体的代码。但是注意,如果是静态方法,那么执行这个方法时查找的锁就是类锁了,而不是对象锁了。这种用法虽然直接使用了this和整个方法体中的代码,但是也可以看情况使用,满足这个使用条件的就可以使用这个方式,代码也会更简洁。
public synchronized void myFunc(){ .... }
类定义中的synchronized:如果synchronized关键字出现在类的定义修饰符中,那么表示这个类创建的所有对象都拥有同一个锁,也称之为类锁,类锁的定义主要是为了保证静态变量的线程安全。
6. 死锁
public class ThreadTest{ public static void main(String[] args){ Object o1 = new Object(); Object o2 = new Object(); MyThread1 t1 = new MyThread1(o1, o2); MyThread2 t2 = new MyThread2(o1, o2); t1.start(); t2.start(); } } class MyThread1 extends Thread{ Object o1; Object o2; public MyThread1(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized(o1){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 此时想要去获取o2的锁,但是已经被MyThread2的线程获取了,只能暂定并等待 synchronized(o2){ System.out.println("MyThread1 run..."); } } } } class MyThread2 extends Thread{ Object o1; Object o2; public MyThread2(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized(o2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 此时想要去获取o1的锁,但是已经被MyThread1的线程获取了,只能暂定并等待 synchronized(o1){ System.out.println("MyThread2 run..."); } } } }
7. 定时器
8. wait和notify
wait和notify方法不是线程Thread类的方法,而是Object的方法,即任何类都有这两个方法。但是注意,这两个方法是建立在线程同步的机制上的。
- wait():让正在该对象(调用wait方法的对象)上活动的线程(当前线程)进入无限期等待的状态,直到被唤醒为止。并且该线程会释放在该对象上占有的锁。
- notify():唤醒正在该对象(调用notify方法的对象)上等待的线程。但是注意,这个方法只是起到通知的作用,并不会释放在该对象上占有的锁,通常是线程执行完毕就自动释放了。
- notifyAll():唤醒所有正在该对象(调用notify方法的对象)上等待的线程,这个方法同样不会释放对象锁。