Java中的多线程基础
1、线程与进程
进程:
- 进程是程序运行以及资源分配的基本单位,一个程序至少有一个进程。
- 如下图所示:
线程:
- 线程是CPU调度和分配的基本单位,一个进程至少有一个线程。
- 同一个进程中的线程共享进程资源(减少切换,可提高效率),且可以并发执行。
2、并发和并行
并发:
- 指的是同一时间间隔执行多个事件,强调的是一段时间内可以做多个不同的事。
- 比如在一分钟内,先吃一口饭,再喝一口水,接着说一句话等等。
并行:
- 指的是同一时刻执行多个事件,强调的是同一时刻可以做多个不同的事。
- 比如在吃饭的同时,一边听歌,看电视。
3、线程的上下文切换
基本原理:
- 一个CPU在任意时刻只能执行一个线程,如果有多个线程(超过CPU个数)存在,CPU将会采用时间片轮转的方式进行线程切换,即给每一个线程分配一个时间片,当一个线程的时间片用完的时候便会处于就绪状态,并让出CPU给其他线程使用,这就是一次上下文切换。
- 上下文切换时是通过运行时数据区中的程序计数器来存储各个线程的运行状态的,以便于下次运行线程时可以接着上次指令继续运行(比如线程A做了一次计算准备返回数据时,切换到了线程B,然后又切回线程A,是直接返回数据,而不是再去计算一遍)。
- 程序计数器指的是JVM中的一块内存区域,它可以看作是当前线程所执行字节码的行号指示器,通过它,Java可以知道每个线程执行到了哪一步指令(由此可以看出程序计数器是线程私有的)。
- 一般而言,上下文切换是比较耗费CPU时间的一种操作。
4、线程的几种状态
基本原理:
- 创建:指的是生成线程对象,此时并没有调用start方法。
- 就绪:调用线程的start方法后,线程便进入了就绪状态,此时等待系统调度。
- 运行:通过系统调度,开始运行线程中的run函数。
- 等待:调用了Object.wait()会进入等待队列。一般需要等待其他线程做出一些特定的通知或者中断。
- 超时等待:该状态与等待状态有一点区别就是它会自动返回,比如调用Threa.sleep(long),在超时之后,会自动返回进入就绪状态。
- 阻塞:指的是去获取锁时(等待进入synchronized方法或块),发现同步锁被占用了,这时线程会被放入锁池,等待获取锁。
- 死亡:运行完run方法,main方法(main方法指的是主线程)之后正常退出,也有可能出现异常导致死亡;死亡的线程不能重新启用,否则报错。
5、创建线程的几种方式
继承Thread类:
- 创建一个类继承Thread,重写其中的run方法,即线程执行体。
- 创建继承了Thread类的子类实例,即线程对象
- 调用线程对象的start()方法启动线程。
- 示例代码如下;
public class ThreadTest { public static void main(String[] args){ new TestThread().start(); } } class TestThread extends Thread{ @Override public void run(){ System.out.println("Test Thread"); } }
实现Runnable接口:
- 创建一个类实现Runnable接口,重写其中的run方法,即线程执行体。
- 创建实现Runnable接口的类实例,接着将该实例对象作为target传入Thread类的构造器,此时创建的Thread类才是线程对象。
- 调用线程对象的start()方法启动线程。
- 示例代码如下;
public class ThreadTest { public static void main(String[] args){ new Thread(new TestRunnable()).start(); } } class TestRunnable implements Runnable{ @Override public void run() { System.out.println("Test Runnable"); } }
实现Callable类:
- 创建一个类实现Callable接口,重写其中的call方法,即线程执行体,call方法存在返回值。
- 创建实现Callable接口的类实例,将该实例对象作为callable传入FutureTask类的构造器,此FutureTask对象封装了该Callable对象的call()方法的返回值,可使用get方法获取。
- 将FutureTask实例对象作为target传入Thread的构造器,此时创建的Thread类才是线程对象。
- 调用线程对象的start()方法启动线程。
- 示例代码如下;
public class ThreadTest { public static void main(String[] args){ FutureTask<Integer> task = new FutureTask(new TestCallable()); new Thread(task).start(); try { System.out.println(task.get());//get方法会得到call执行完之后的值 } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class TestCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int num = 0; while(num < 5){ num++; } return num; } }
6、sleep() 和 wait()
sleep():
- sleep()方法是线程类Thread类的一个静态方法,如若是在synchronized中调用,sleep()方法不会释放锁。
wait():
- wait()方法是线程类Object类的一个方法,如若是在synchronized中调用,wait()方法会释放锁。
- 示例代码如下(从控制台输出的时间戳结果可以看出sleep没有释放锁,是阻塞进行的,而wait释放了锁):
public class ThreadTest { protected static volatile Object lock = "lock"; public static void main(String[] args){ for(int i=0;i<5;i++){ new Thread(() -> { synchronized(lock){ System.out.println(Thread.currentThread().getName() + ":等待开始当前时间戳:" + System.currentTimeMillis()); try { // lock.wait(1000);//让当前线程进入等待池 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } }
7、关于wait() 与 notify(),notifyAll()
notify(),notifyAll():
- notify()指的是随机唤醒一个wait线程进入对象锁池中竞争锁,而notifyAll()是唤醒所有的wait线程进入对象锁池中竞争锁。
- 关于wait()与notify()方法示例代码如下:
public class ThreadTest { protected static volatile Object lock = "lock"; public static void main(String[] args){ new Thread(new TestRunnable(lock)).start(); try { Thread.sleep(1000); System.out.println("sleep 1000ms"); } catch (InterruptedException e) { e.printStackTrace(); } new TestThread(lock).start(); } } class TestRunnable implements Runnable{ private Object lock; public TestRunnable(Object lock){ this.lock = lock; } @Override public void run() { try { synchronized (lock){ System.out.println("begin wait:" + System.currentTimeMillis()); System.out.println("Release lock........"); lock.wait();//让当前线程进入等待池 System.out.println("end wait:" + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } } class TestThread extends Thread{ private Object lock; public TestThread(Object lock){ this.lock = lock; } @Override public void run() { synchronized (lock){ System.out.println("begin notify:" + System.currentTimeMillis()); System.out.println("do something........"); lock.notify();//从等待池中随机唤醒一个wait线程进入锁池竞争锁 // lock.notifyAll();//从等待池中随机唤醒所有wait线程进入锁池竞争锁 System.out.println("end notify:" + System.currentTimeMillis()); } } }
8、start() 和 run()
start():
- 此方法是用来启动一个线程的,执行了start()方法之后,线程将进入就绪状态,之后会自动执行run函数中的内容,无需等待,是真正意义上的实现了多线程。
run():
- 单独运行此方法,只是将其当做一个普通函数在使用,每次执行必须等待run函数里面的内容完成,才能接着往下执行,无法实现多线程。
9、线程安全性
主要体现在下面三方面:
- 原子性:同一时刻只允许一个线程对数据进行操作(atomic开头的原子类,synchronized,Lock)。
- 可见性:一个线程对共享变量的修改,可以及时地被其他线程观察到,(synchronized,volatile,Lock)。
- 有序性:指的是可以观察到其他线程的指令执行顺序,由于指令重排,一般情况下是无序的(happens-before原则)。
10、线程死锁
死锁:
- 指的是多个线程在执行过程中,因争夺资源而陷入环路阻塞的一种现象。
- 示例代码如下(运行之后,两个线程陷入死锁中,如果没有外力中断,将会一直锁定):
public class ThreadTest { //共享资源A private static Object A = new Object(); //共享资源B private static Object B = new Object(); public static void main(String[] args){ new Thread(() -> { synchronized (A){ System.out.println(Thread.currentThread().getName() + ":得到资源---A"); try { Thread.sleep(1000);//此处休眠是为了让其他线程获得执行机会 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":去获取资源---B"); synchronized (B){ System.out.println(Thread.currentThread().getName() + ":得到资源---B"); } } },"Thread-01").start(); new Thread(() -> { synchronized (B){ System.out.println(Thread.currentThread().getName() + ":得到资源---B"); try { Thread.sleep(1000);//此处休眠是为了让其他线程获得执行机会 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":去获取资源---A"); synchronized (A){ System.out.println(Thread.currentThread().getName() + ":得到资源---A"); } } },"Thread-02").start(); } }
死锁四个必要条件即如何避免死锁:
- 互斥条件:一个资源在任意时刻只能被一个线程占用,如果有其他线程请求该资源,需要等待原线程释放资源(此条件不能被破坏,因为锁本身就是为了互斥访问)。
- 请求和保持条件:一个线程已经获取了一部分资源,又去请求其他资源,如果其他资源正被占用,原线程陷入阻塞,且不会释放自己占用的资源(一次性申请所有资源;或者阻塞时,释放自己占用的资源)。
- 不可剥夺条件:一个线程已经获得的资源,在自己未使用完之前不能被其他线程剥夺,只能自己使用结束后释放(如果去获取正在被其他线程使用的资源而阻塞时,可以释放自己占用的资源)。
- 环路等待条件:多个进程之间形成一种头尾相接的循环等待资源关系(按一定的顺序来获取资源)。
- 针对上述例子而言,可以让现线程1先获取资源AB,执行完之后,再让线程2获取资源AB,改动如下(执行顺序为:线程1先获取资源A,接着释放CPU,线程2执行准备去获取资源A,发现资源A已被占用,此时线程2阻塞,然后一秒之后释放CPU,线程1接着执行,去获取资源B,正常获取,执行完毕,释放CPU;线程2开始获取资源A,由于线程1已经执行完毕释放了锁,所以线程2正常获取资源A,接着休眠一秒释放CPU,然后又去获取资源B,同样正常获取,执行完毕):
public class ThreadTest {
//共享资源A
private static Object A = new Object();
//共享资源B
private static Object B = new Object();
public static void main(String[] args){
new Thread(() -> {
synchronized (A){
System.out.println(Thread.currentThread().getName() + ":得到资源---A");
try {
Thread.sleep(1000);//此处休眠是为了让其他线程获得执行机会
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":去获取资源---B");
synchronized (B){
System.out.println(Thread.currentThread().getName() + ":得到资源---B");
}
}
},"Thread-01").start();
new Thread(() -> {
//线程2去获取A时被阻塞
synchronized (A){
System.out.println(Thread.currentThread().getName() + ":得到资源---A");
try {
Thread.sleep(1000);//此处休眠是为了让其他线程获得执行机会
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":去获取资源---B");
synchronized (B){
System.out.println(Thread.currentThread().getName() + ":得到资源---B");
}
}
},"Thread-02").start();
}
}
(以上所有内容皆为个人笔记,如有错误之处还望指正。)