线程
概念
进程:系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是一个独立的执行单元,具有自己的内存空间、代码和数据,其中资源包括CPU、磁盘IO、内存等等。也可以称为一个程序运行的实例。
线程:线程是进程中的一个执行单元,有时也被称为轻量级进程(Lightweight Process,LWP)。它是CPU调度和分派的基本单位。
CPU时间片轮转机制:又称为RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
上下文切换:可以理解成内核(操作系统的核心)在CPU上对进程(含线程)进行切换,上下文切换过程中的信息保存在进程控制模块中(即PCB)
上下文切换活动:
- 挂起一个进程,将这个进程的状态(上下文)存储到某个内存中。
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器上恢复。
- 跳转到进程被中断时的代码行,以恢复该进程。
上下文切换的原因
- 当前执行的任务的时间片用完了,CPU正常调度下一个任务。
- 当前任务遇到阻塞,调度器将任务挂起,继续调度下一个任务。
- 多个任务抢占锁资源,当前任务没有抢到资源,被挂起。
- 用户代码挂起任务,让出CPU。
- 硬件中断。
Java中的线程
线程实现和创建方式
Java中对线程的抽象是Thread,而Runnable只是业务逻辑的抽象,可以将任务交给线程执行。
线程的创建和启动有如下两种方式(官方文档注释说明)
- 继承Thread,然后调用start()启动线程。
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread run");
}
}
MyThread myThread = new MyThread();
myThread.start();
- 实现Runnable,然后将任务交给thread进行执行。
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable run");
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
实现Callable创建线程
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,可以说任务是通过线程驱动从而执行的。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "今天天气真好";
}
}
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
线程中止方式
- 正常运行结束
程序正常运行结束,线程就会停止。 - 通过interrupt方式
有一些线程需要长时间运行,在满足一些条件下才会中止,可以使用中断标志的方式进行和谐中止。
1、线程未处于阻塞状态,通过interrupt标志判断是否需要中止程序。
public class MyThread extends Thread{
@Override
public void run() {
//通过isInterrupted标志进行判断
while (!isInterrupted()) {
String name = currentThread().getName();
System.out.println(name+" is runing");
}
System.out.println("MyThread end");
}
}
MyThread myThread = new MyThread();
myThread.start();
try {
Thread.sleep(100);
//发送中断标志
myThread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
2、线程处于阻塞状态,如调用sleep(),这时调用interrupt()会抛出InterruptedException异常,这时中断标志会被清除,如果想中止线程,需要再次中断。
@Override
public void run() {
while (isInterrupted()){
try {
String name = currentThread().getName();
System.out.println(name+" is runing");
//休眠阻塞
sleep(1000);
} catch (InterruptedException e) {
//中断标志清除,变成false
boolean is = isInterrupted();
//如果想中止,需要再次中断
interrupt();
e.printStackTrace();
}
}
System.out.println("MyThread end");
- 通过stop方式
该方法已经过期,不推荐使用,这种属于暴力停止,线程不安全。
start和run的区别
- 通过new Thread创建线程的实例,这时候线程并未真正起来,需要调用start()后通过底层才真正起来。线程调用start后处于就绪状态,等待分配CPU,等分配到cpu的时候然后执行run。
- run只是执行任务逻辑的地方,本质跟普通方法没有区别。
sleep和wait的区别
- sleep是Thread里面的方法,wait是Object的方法。
- sleep是导致程序暂停执行指定时间,让出CPU,当指定时间到了之后会恢复到就绪状态。
- sleep过程不会释放对象锁。
- 调用wait后会进入等待状态,会放弃对象锁。通过notify方法才会被唤醒。
线程的状态(生命周期)
- 新建状态(New)
通过new Thread创建线程后,该线程处于新建状态,JVM分配内存,及相关初始化。 - 就绪状态(Runnable)
调用start()后处于就绪状态,等待分配CPU调度运行。 - 运行状态(Running)
就绪状态的线程获取到cpu资源,开始执行run逻辑体,这时处于运行状态。 - 阻塞状态(Blocked)
1、线程尝试进入一个被其他线程锁定的同步代码块或方法时,会进入BLOCKED状态。 2、BLOCKED状态的线程无法继续运行,直到获得锁。线程需要等待锁被释放。 - 无限期等待(Waiting)
1、线程进入等待状态,直到被其他线程显式唤醒。
2、线程在WAITING状态下会一直等待,直到被显式唤醒,如通过notify()、notifyAll()方法,或在join()方法中目标线程运行结束后自动唤醒。
3、处于WAITING状态的线程不会消耗CPU资源。
进入该状态的方法 | 退出该状态 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
- 限时等待(Timed Waiting)
1、线程进入等待状态,但设置了超时时间,超时后线程会自动唤醒。
2、调用以下方法时线程进入TIMED_WAITING状态:Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)、LockSupport.parkNanos()、LockSupport.parkUntil()。
3、超时时间结束后,线程转为RUNNABLE状态,或者由其他线程显式唤醒。
进入该状态的方法 | 退出该状态 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束、 Object.notify() 、Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 、被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
- 死亡状态(Dead)
1、run方法执行完后,线程就中止了。
2、异常结束,线程抛出异常或者error。
3、调用stop停止(不推荐使用)。
线程锁
在Java中,线程锁(Thread Lock)是一种用于控制多个线程对共享资源访问的机制。通过线程锁,可以确保在同一时间只有一个线程可以访问某个共享资源,从而避免并发问题,如数据不一致、资源竞争等。Java 提供了多种实现线程锁的机制,主要包括 synchronized 关键字和显式锁(如 ReentrantLock)。
内置锁(synchronized)
内置锁是最简单、最常用的锁机制,它通过synchronized关键字来实现。它可以用于方法或代码块。synchronized是可重入锁,同一个线程可以多次获得同一个锁而不会发生死锁。
synchronized关键字修饰的三种方式
- 修饰静态方法
静态方法相当于类锁,静态不属于任何实例对象。
//懒汉式单例模式
public class LazySingleton {
public static LazySingleton instance = null;
private LazySingleton(){
}
//通过synchronized加锁,这个是类锁即LazySingleton.class
public static synchronized LazySingleton getInstance(){
//如果不加锁,有可能Thread-0创建单例时因为某种原因暂停,另外一个线程进入后会重复创建,此时就不是单例了
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
- 修饰实例方法
public class SynchronizedDemo {
public void method(){
//属于对象实例
synchronized (this){
System.out.println("Synchronized");
}
}
}
/*对象锁*/
public class CountSync {
private int count = 0;
//在方法用synchronized进行加锁,此时默认是一把对象锁即this
public synchronized void addCount(){
count++;
}
public void addCount1(){
synchronized(CountSync.this){
count++;
}
}
private Object object = new Object();
public void addCount2(){
synchronized(object){
count++;
}
}
private static class ThreadCount extends Thread {
private CountSync countSync;
public ThreadCount(CountSync countSync){
this.countSync = countSync;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
countSync.addCount();
}
}
}
public static void main(String[] args) throws InterruptedException {
CountSync countSync = new CountSync();
ThreadCount threadCount1 = new ThreadCount(countSync);
ThreadCount threadCount2 = new ThreadCount(countSync);
threadCount1.start();
threadCount2.start();
Thread.sleep(100);
System.out.println(countSync.count);
}
}
- 修饰代码块
//通过synchronized加锁,这个是类锁即LazySingleton.class
public static LazySingleton getInstance2(){
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
synchronized的底层原理
- synchronized 修饰方法的的情况
public class SynchronizedTest1 {
public synchronized void test() {
System.out.println("synchronized修饰方法");
}
}
通过javac和javap编译和反编译命令可以查看到synchronized 修饰的方法是通过ACC_SYNCHRONIZED标志说明该方法是一个同步方法。
- synchronized 修饰代码块的的情况
synchronized 修饰代码块的实现使用的是 monitorenter 和 monitorexit 指令。
1、monitorenter指令
功能:当线程执行到monitorenter指令时,它会尝试获取指定对象的监视器锁(monitor)。
获取锁的过程:
1)如果该对象的监视器锁未被占用(即进入数为0),则该线程会进入监视器,并将进入数设置为1,此时该线程成为监视器的所有者。
2)如果线程已经占有该监视器(只是重新进入),则进入监视器的进入数会加1,这体现了锁的可重入性。
3)如果其他线程已经占用了该监视器,则该线程会被阻塞,直到监视器的进入数为0时,再重新尝试获取监视器的所有权。
2、monitorexit指令
功能:当线程执行到monitorexit指令时,它会释放之前获取的监视器锁。
释放锁的过程:
1)执行monitorexit指令的线程必须是该监视器的所有者。
2)指令执行时,监视器的进入数会减1。
3)如果减1后进入数为0,则该线程退出监视器,不再是这个监视器的所有者,此时其他被这个监视器阻塞的线程可以尝试去获取这个监视器的所有权。
显式锁(ReentrantLock)
ReentrantLock提供了比synchronized更灵活的锁机制,比如可以尝试获取锁、可中断的锁获取、超时获取锁等。是可重入锁,同一个线程可以多次获得同一个锁而不会发生死锁。
public class ReentrantLockCount {
private int count = 0;
private final Lock mLock = new ReentrantLock();
public void addCount(){
//获取锁
mLock.lock();
try {
count++;
}finally {
//释放锁
mLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockCount reentrantLockCount = new ReentrantLockCount();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
reentrantLockCount.addCount();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
reentrantLockCount.addCount();
}
});
thread1.start();
thread2.start();
Thread.sleep(100);
System.out.println(reentrantLockCount.count);
}
}
synchronized和ReentrantLock的区别
- 两者都是可重入锁。
可重入锁:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 - 实现方式不一样
synchronized 是基于 JVM 实现的,JDK1.6之后 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - ReentrantLock新增了一些功能
1、等待可中断
通过lock.lockInterruptibly()来实现这个机制。比如说正在等待的线程可以选择放弃等待,改为处理其他事情。
2、ReentrantLock可以设置公平锁
公平锁就是先等待的线程先获得锁。ReentrantLock默认是非公平,可以通过构造方法ReentrantLock(boolean fair)来设置,而synchronized只能是非公平。