一、线程与进程
线程是指计算机中能够执行独立任务的最小单位。它是进程的一部分,一个进程可以包含多个线程。每个线程都是独立运行的,它们共享进程的资源,如内存空间和文件句柄等。线程之间可以通过共享内存进行通信,因此线程之间的切换开销较小。
进程是计算机中执行任务的基本单位,它是程序在执行过程中的一个实例。一个进程可以包含多个线程,每个线程执行不同的任务。进程之间是独立的,它们有自己独立的地址空间和资源。进程之间的通信需要经过额外的机制,如管道、消息队列等。
总结来说,线程是进程的一部分,是计算机中执行任务的最小单位。进程负责管理和分配资源,线程负责具体的任务执行。线程之间共享进程的资源,进程之间需要通过额外的机制进行通信。
二、线程类Thread
1. 线程是程序运行的不同路线。
2.自定义线程时需要继承Thread类或实现Runable接口,重写run()方法。
class ThreadA extends Thread{
//重写run方法,定义线程要执行的任务
@Override
public void run(){
for(int i=0;i<=20;i++){
System.out.println(i+Thread.currentThread().getName());
}
}
}
子类抛出的异常只能比父类更精确,如果父类没有异常,子类的异常不能抛出,要处理
3.实例化线程对象使用 线程对象.start();开启线程。
Thread a=new ThreadA();
Thread b=new ThreadA();
//开启线程
a.start();
b.start();
4.线程常用的方法
(1). 休眠线程sleep(); 休眠结束会自动启动线程
Thread.sleep(5000);//休眠5秒
(2). 获取当前线程对象 Thread.currentThread();
System.out.println(Thread.currentThread().getName());
(3). 设置优先级 setPriority(5);优先级越高,获取CPU资源的几率越大,并不是优先级高先执行。
Thread a=new ThreadB();
a.setPriority(5);//优先级 1-10 默认是5 设置其他值报错
(4).礼让 yeild();
作用:让出CPU资源,让CPU重新分配 防止一条线程长时间占用CPU资源,达到CUP资源合理分配的效果 sleep(0)也具有同样的效果Thread.yield();//执行到这句的线程让出cpu资源,让cpu重新分配
(5).join() 成员方法 加入(插队)
在A线程中执行了B.join() B线程运行完毕后,A线程再运行
Thread t;
t.join();//插队
(6).判断线程是否存活isAlive();
5. 关闭线程
1. 线程对象.stop(); 立即关闭线程。
a.stop();//不推荐
2.线程对象.stop=true,自定义一个状态属性,在线程外部设置此属性,影响线程内部的运行
a.stop=true;
class ThreadG extends Thread{ volatile boolean stop=false;//不稳定的,声明这个属性的值可能发生变化 @Override public void run(){ while (!stop){ //System.out.println("A"); } } }
volatile关键字:不稳定的,易变的,声明这个属性的值可能发生变化
volatile
关键字用于修饰变量,表示该变量是易变的(volatile变量)。当一个变量被声明为volatile
时,它会告知编译器和虚拟机该变量可能会被多个线程同时访问并修改,因此需要特别注意线程可见性和指令重排序等问题。对于使用了
volatile
修饰的变量,在线程外部对其进行修改后,其他线程在访问该变量时能够立即看到最新的值,而不会使用缓存中的旧值。使用
volatile
关键字的一个常见应用场景是用于控制线程的停止标志。例如,我们可以定义一个volatile
变量作为线程的停止标志,在外部设置该变量的值为true
时,线程内部通过检查该变量的值来决定是否停止执行。public static void stopThread(){ ThreadG a = new ThreadG(); a.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } a.stop = true; System.out.println("设置关闭"); }
3.线程对象.interrupt(); 调用interrupt();设置中断状态,这个线程不会中断,我们需要在线程内部判断,中断状态是否被设置,然后执行中断操作
@Override public void run(){ for(int i=0;i<100;i++){ if(Thread.currentThread().isInterrupted()){//判断中断状态 break; } try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(i); } }
6. volatile关键字在多线程编程中起到了两个主要作用:
-
可见性:如果一个变量被声明为volatile,当一个线程修改了该变量的值,其他线程可以立即看到变量的最新值。普通的变量在多线程环境下可能存在数据不一致的问题,因为线程A修改了变量的值,但是线程B在此之前已经缓存了该变量的旧值,导致线程B读取的是一个过期的值。使用volatile关键字可以避免这种情况发生,保证变量的可见性。
-
有序性:普通的变量在多线程环境下可能存在指令重排序的问题。指令重排序是指处理器为了提高执行效率,可能会对指令进行重新排序执行,但是重排序可能会导致代码的执行顺序与预期不一致。使用volatile关键字可以禁止指令重排序,保证代码的执行顺序与程序员编写的顺序一致。
需要使用volatile关键字的主要原因是多线程环境下的可见性和有序性问题。在多线程编程中,为了保证数据的一致性和正确性,需要使用volatile关键字来解决这些问题。
三、线程的生命周期
线程的生命周期可以简要概括为以下几个状态:
- 新建(New):线程被创建但尚未开始执行。
- 就绪(Runnable):线程处于可运行状态,等待被分配CPU时间片以执行。此时可能有多个线程处于就绪状态,但只有一个线程能够获得CPU时间片执行。
- 运行(Running):线程获得了CPU时间片,正在执行线程体中的代码。
- 阻塞(Blocked):线程因为某些原因被阻塞,暂时停止执行。可能的原因包括等待某个操作完成、等待输入/输出、等待锁等。当阻塞条件满足时,线程会重新进入就绪状态。
- 等待(Waiting):线程因为调用了
wait()
方法,主动让出CPU并进入等待状态,直到其他线程调用了相同对象的notify()
或notifyAll()
方法才能被唤醒。 - 超时等待(Timed Waiting):线程因为调用了带有超时参数的
wait()
、join()
或sleep()
方法,进入具有超时等待时间的等待状态。当超时时间到达或其他线程唤醒它时,线程会重新进入就绪状态。 - 终止(Terminated):线程执行完了所有的代码或者出现了未捕获的异常而意外终止。
四、线程安全
同步(Synchronous)指的是线程按照顺序依次执行,前一个线程执行完毕后,下一个线程才能开始执行。同步可以保证线程之间的操作按照一定的顺序和规则进行,从而避免竞争条件和数据不一致的问题。常用的同步机制包括使用锁(如synchronized关键字)、信号量、互斥量等。
异步(Asynchronous)指的是线程在执行任务时,不需要等待上一个任务的完成,而是同时执行多个任务。异步通常通过回调函数、事件驱动等方式实现。异步执行可以提高程序的性能和响应速度,但也需要考虑线程安全问题。
1. 线程安全:多个线程操作一个对象,不会出现结果错乱的情况(缺失)。
StringBuilder是线程不安全的,StringBuffer是线程安全的。
public class SyncThreadA {
//StringBuilder就是线程不安全
public static void main(String[] args) {
StringBuilder strB=new StringBuilder();
//线程可以执行的任务
RunA r=new RunA(strB);
Thread a=new Thread(r);
a.start();
Thread b=new Thread(r);
b.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(strB.length());//执行结果不为2000,StringBuilder线程不安全
}
}
//实现Runnable接口
class RunA implements Runnable{
StringBuilder strB;
public RunA(StringBuilder strB){
this.strB=strB;
}
@Override
public void run() {
for(int i=0;i<1000;i++){
strB.append("0");
}
}
}
2. 要做到线程安全,我们可以使用 synchronized,对方法或者代码块加锁,达到线程同步的效果。
使用synchronized关键字修饰的方法或代码块,同一时间内,只能允许一个线程执行此代码。
static synchronized void syncMethod(){ }
synchronized也可以修饰代码块
static void syncBlock() { synchronized (this) { System.out.println("进入同步代码块" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束同步代码块" + Thread.currentThread().getName()); } }
锁对象 使用synchronized需要指定锁对象 synchronized修饰方法 如果成员方法 锁对象this 如果静态方法 锁对象为类的类对象 如obj.getClass() 类名.class
五、锁的分类
1. 以下是几种常见的锁分类:
-
悲观锁和乐观锁:悲观锁认为在并发情况下会发生冲突,所以默认加锁来保护共享资源。乐观锁则认为并发冲突的概率较低,采用无锁或轻量级锁的方式来实现并发控制。悲观锁有锁对象,乐观锁没有锁对象。
-
公平锁和非公平锁:公平锁按照线程请求锁的顺序进行获取,保证了先来后到的公平性。非公平锁则允许线程插队,不保证获取锁的顺序,提高了吞吐量。
-
可重入锁和不可重入锁:可重入锁允许同一个线程多次获取同一个锁对象的锁。当线程已经持有锁时,再次获取锁时不会被阻塞,而是增加锁的计数。不可重入锁则不允许同一个线程多次获取同一个锁。(可重入锁在同步代码块中遇到相同的锁对象的同步代码块,不需要再获取锁对象的权限,直接进入执行;Java里面全部都是可重入锁)
-
偏向锁,轻量级锁(自旋锁)和重量级锁:偏向锁适用于只有一个线程访问同步代码块的情况;轻量级锁适用于多个线程竞争同步代码块的情况,但竞争不激烈的场景;而重量级锁适用于竞争激烈的场景,多个线程频繁竞争同步代码块的情况。
偏向锁是一种针对线程访问同步代码块的优化手段。当一个线程获取了一个同步代码块的锁之后,如果没有其他线程竞争该锁,则持有锁的线程会偏向于该锁,在以后的访问中无需再经过同步操作,提高了性能。
轻量级锁(自旋锁)是一种针对多个线程竞争同步代码块的优化手段。当一个线程尝试获取一个同步代码块的锁时,如果当前锁处于偏向状态且偏向线程是当前线程,则可以直接获取锁而无需进入阻塞状态。如果当前锁不处于偏向状态,或者处于偏向状态但是偏向线程不是当前线程,则需要使用自旋等待锁的释放,而不阻塞线程。
重量级锁是一种针对多个线程竞争同步代码块的一种常规锁机制。当多个线程尝试获取同一个锁时,除了自旋等待锁的释放外,还会将未获取到锁的线程阻塞起来,直到锁的持有者释放锁,被阻塞的线程才能继续执行。
synchronized关键字可以被看作是一种悲观锁,使用锁对象来实现线程同步。它确保在同一时间只有一个线程可以获取到锁对象,并进入synchronized代码块执行。其他线程需要等待锁对象被释放后才能进入。
synchronized关键字默认是非公平锁,也就是说,不会按照线程的启动顺序来获取锁,而是随机的。当多个线程同时竞争锁时,并不保证先尝试获取锁的线程一定会获得锁。
synchronized关键字是可重入锁,同一个线程可以重复获取锁对象。在同步代码块中,遇到相同的锁对象的同步代码块,不需要再次获取锁对象的权限,而是直接进入执行。
关于synchronized的锁类型,涉及到锁的状态。当没有竞争时,synchronized使用偏向锁来优化,如果有竞争,升级为轻量级锁(自旋锁),如果自旋不成功,则进一步升级为重量级锁。
综上所述,synchronized关键字可以被看作是一种悲观锁,使用锁对象实现线程同步。它是非公平锁,支持可重入性,并且在不同的竞争情况下可能升级为不同的锁类型。
乐观锁是一种并发控制机制,用于解决多线程同时访问共享资源可能导致的数据不一致问题。乐观锁的实现方式有两种常见的方式:CAS(Compare and Swap)和版本号控制。
CAS(比较并交换)是一种无锁并发控制方式,它通过比较共享资源的当前值与期望值是否相等来判断其他线程是否修改了该值。如果相等,则将共享资源的值更新为新值,如果不相等,则表示有其他线程已经修改了该值,此时需要重新读取共享资源的值并重试。CAS操作是原子的,因此可以确保并发安全。
版本号控制是通过为共享资源增加一个版本号来实现的。每次修改共享资源时,都会更新版本号。当一个线程要修改共享资源时,首先读取共享资源的版本号,并保存为当前版本号。如果其他线程在此期间已经修改了共享资源,则当前版本号与保存的版本号不相等,此时需要放弃修改操作。通过版本号的比较,可以确保只有最新的修改才能成功。
两种方式各有优缺点。CAS方式由于没有锁的开销,在低并发情况下性能较好,但在高并发情况下会导致大量的重试,降低效率。版本号控制方式则可以避免重试,但需要维护额外的版本号字段,增加了存储和计算开销。
综上所述,选择使用哪种乐观锁实现方式,需要根据具体的应用场景和性能需求来决定。
六、BIO、NIO、AIO
1. BIO、NIO、AIO 是 Java 编程语言中用于处理网络通信的三种不同的 I/O 模型。
BIO(Blocking I/O)是传统的同步阻塞式 I/O 模型。在这种模型中,每个连接都要创建一个线程进行处理,当有大量连接时,会导致线程数过多,资源消耗增加。
NIO(Non-blocking I/O)是一种基于事件驱动的同步非阻塞 I/O 模型。在这种模型中,I/O 操作不会阻塞线程,而是将 I/O 事件通知给选择器(Selector),然后通过一个或多个线程来处理这些事件。
AIO(Asynchronous I/O)是一种异步非阻塞 I/O 模型。它将数据的读写操作交给操作系统内核来处理,不需要通过线程池或者线程来阻塞等待结果,而是在操作完成后通过回调函数的方式来将数据返回给应用程序。
在高并发的场景下,NIO 和 AIO 的性能会比 BIO 更好。
2. 同步与异步的区别在于:
同步:请求与响应同时进行,直到响应再返回结果;
异步:请求直接返回空结果,不会立即响应,但一定会有响应,通过通知、状态、回调函数响应
3. 阻塞与非阻塞的区别在于:
阻塞:请求后一直等待
非阻塞:请求后,可以继续干其他事,直到响应