目录
多线程
线程与进程
- 程序:程序是一个静态概念,一般是指一个可执行文件。
- 进程:执行的程序叫做进程,是一种动态概念。
- 线程:线程是进程的一个执行单元,一个程序有可以有多个线程,一个线程只属于一个进程。
- 进程与线程的区别:进程是资源分配的单位,线程是执行和调度的基本单位。线程可以看成一种轻量级的进程。
- 多线程的优点:参考多线程的优点
线程的生命周期
线程生命周期分为五个阶段:
新生状态:利用new关键字创建线程后,线程处于新生状态。
Thread t1 = new Thread();
就绪状态:此时线程已具备除CPU资源外的所有运行条件。
当启动时,线程就处于就绪状态。而sleep(),wait(),join()结束后也进入就绪状态。
t1.start
运行状态:线程拿到CPU资源正在执行。
run();
阻塞状态:暂停一个正在执行的线程。
t1.wait();
t1.sleep(100)
死亡状态:线程执行完毕后或意外终止后被销毁。
run方法执行完毕或者使用t1.stop()
方法异常终止。
线程的常用方法
线程操作方法
start():线程开始,调用此方法后线程进入就绪状态。
yield():暂停当前执行的线程,让出CPU资源,不释放锁,进入就绪状态。
sleep():线程进入休眠状态,释放CPU资源,不释放锁,进入阻塞状态,时间结束后进入就绪状态。
wait():线程暂时停止进入等待池,释放CPU资源和锁,进入阻塞状态,等待notify()唤醒。
notify():唤醒线程,进入就绪状态。
notifyAll():唤醒等待池中的所有线程,进入就绪状态。
join():释放原线程的CPU和锁,用于自身线程运行直至执行完毕。
jion():暂停当前线程,开始指定线程。
Thread t1 = new Thread();
t1.start();
Thread t2 = new Thread();
t2.join(); //暂停t1,开始t2
这里用一个例子来说明一下
public class TestThreadJoin {
public static void main(String[] args) {
System.out.println("唐僧化缘的故事");
Thread t1 =new Thread(new TangThread());
t1.start();
}
}
class TangThread implements Runnable {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("唐增饿了");
System.out.println("唐僧让悟空去化缘");
Thread t2 =new Thread(new SunThread());
t2.start();
try {
t2.join();
}catch (Exception e) {
// TODO: handle exception
System.out.println("唐僧被妖怪抓走了");
}
System.out.println("唐僧分了悟空一个馒头");
}
}
class SunThread implements Runnable{
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("孙悟空出发了");
System.out.println("孙悟空化缘需要8分钟");
try {
for(int i=1;i<9;i++) {
System.out.println("第"+i+"分钟");
Thread.sleep(1000);
}
}
catch (Exception e) {
// TODO 自动生成的 catch 块
System.out.println("悟空回花果山当大王去了");
}
System.out.println("悟空化缘到了两个馒头");
}
}
运行结果:
线程信息
1. isAlive():判断线程是否终止,返回boolean型变量
2. getPriority():获得线程优先级指数
3. setPriority():设置线程优先级指数
4. setName():设置线程名字
5. getName():获得线程名字
6. currentThread():取得当前运行线程
7. Thread():构造方法,有无参,也可以传入一个Runnable对象。
优先级
- 设置线程优先级:setPriority()
- 获取线程优先级:getPriority()
- 优先级范围为1-10,默认为5,10级最高
- 线程的优先级高低只是调度的概率高低,不是先后执行的概念
多线程的实现
继承Thread类
**使用继承Thread类实现多线程,**继承该类之后,我们就可以通过重写Run方法的方式来使线程为我们执行操作。
public class Test{
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.start();
Thread t1 = new MyThread();
t1.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(this.getName()+":"+i);
}
}
}
运行结果:
实现Runnable接口
使用实现Runnable接口的方法实现多线程,然后重写run方法的方式使用线程执行我们的操作,但是如果要实现线程的使用,而Runnable的则是作为Thread的target参数来进行使用。
public class Test{
public static void main(String[] args) {
Runnable target=new TestRunnable();
Thread r1=new Thread(target);
Thread r2=new Thread(target);
r1.start();
r2.start();
}
}
class Runnable implements Runnable{
@Override
public void run() {
// TODO 自动生成的方法存根
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
运行结果:
Thread和Runnable的比较
- Thread底层也是实现了Runnable接口,本质上和直接实现Runnable接口没有太大区别,但是继承Thread后就不能继承其他类。
- 实现Runnable接口的方式我们可以去继承其他类,可拓展性强。
- 实现Runnable接口的方式更推荐使用,一个是代码的可拓展性强。
实现Callable接口
Callable接口时jdk1.5之后出现的,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法,可以看作是线程的执行体,但call()方法比run()方法更强大。
- call()方法可以有返回值。
- call()方法可以声明抛出异常。
实现Callable接口有两种实现方式。
一是实现Callable接口后,再用FutureTask类进行包装,再作为Thread的target对象传入Thread中。
二是jdk1.8之后可以直接使用lamda表达式来实现了,不再需要为一个专门的类实现其接口了。
// 实现接口方式
public class Test {
public static void main(String[] args) {
// main线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
// 线程一
TestCallable testCallable = new TestCallable();
FutureTask<Integer> task = new FutureTask<>(testCallable);
Thread t1 = new Thread(task,"线程一");
t1.start();
// 线程一的返回值
try {
System.out.println("线程一的返回值"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class TestCallable implements Callable<Integer>{]
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
return i;
}
}
// 使用lamda表达式方法
public class Test {
public static void main(String[] args){
// main线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
// 线程一
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
return i;
});
Thread t1 = new Thread(task,"线程一");
t1.start();
// 线程一的返回值
try {
System.out.println("线程一的返回值"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程池
频繁的创建和销毁线程会造成巨大的内存损耗,而Java中开辟出了一种管理线程的概念,所有的使用线程从线程池中取出,从而避免线程的频繁创建和销毁。它的优点有:
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
Java中提供了创建线程池的一个类:Executor。而我们创建时,为了追求更好的性能,一般使用它的子类:ThreadPoolExecutor。
构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:超出核心线程数之外的线程的空闲存活时间。
- TimeUnit:空闲存活时间的单位
- workQueue:用来存放待执行的任务。
- ThreadFactory:是一个线程工厂,用来生产线程执行任务。
- Handler:任务拒绝策略
栗子:
public class Test {
public static void main(String[] args) {
Runnable r1 = new MyRunnable();
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, queue);
pool.execute(r1);
pool.execute(r1);
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+": "+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行原理
当需要一个线程资源时,
先去检查核心线程数是否已满,未满,直接调用核心线程去执行任务,若已满,
检查等待队列是否有空间,若有,放入等待队列,若无,检查是否达到最大线程数。
若未达到,启动线程工厂创建非核心线程执行任务,若达到,启动任务拒绝策略拒绝任务执行。
等待队列
等待队列有三种类型,是根据存储结构不同划分的,功能都相同,分别为:
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
创建时需要指定等待队列的大小。
handler拒绝策略
handler作为拒绝策略,线程池已满时会拒绝向外提供线程,而我们自定义的时候不需要手动的去设置,它默认设置的是名为AbortPolicy
的拒绝策略。而这些拒绝策略的的响应机制一般如下:
- AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
- DisCardPolicy:不执行新任务,也不抛出异常
- DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
- CallerRunsPolicy:直接调用execute来执行当前任务
常见的其他线程池
除了通过ThreadPoolExecutor
这种等待队列的机制,为了方便大家的使用,java还提供了其他的线程池机制。这里可以作为简单的了解。
- CachedThreadPool:可缓存的线程池,线程没有核心线程,非核心线程数为无限大。它的本质上和普通的线程创建没有区别,有线程即创建,无则销毁。它只是在其基础上增加了缓存的概念,即不需要执行任务时不立即销毁,等待查看是否有其他任务继续执行。
- SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
- SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
- FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
线程并发与安全
多线程环境下的线程安全问题
在多线程并发执行时,多个线程同时操作一个对象资源从而造成资源的溢出,在这种并发条件下,线程的安全的不到保障,需要我们手动的去保证线程资源的安全性。
而线程安全的本质不是线程安全、应该是内存安全,我们创建对象后,对象存储在堆中,而堆是共享内存,可以被所有线程访问,这也就造成了多线程下不安全的问题。要解决此问题,应该从同时只允许一个线程访问该内存资源着手。
线程不安全栗子
public class Test {
public static void main(String[] args) {
Runnable r1 = new MyRunnable();
Thread t1 = new Thread(r1,"窗口一");
Thread t2 = new Thread(r1,"窗口二");
Thread t3 = new Thread(r1,"窗口三");
Thread t4 = new Thread(r1,"窗口四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true){
try{
// 模拟卖票时的操作所需的延迟
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
if (ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"卖出了一张票,票数为:"+ticket--);
}
}
}
在这个例子中,我们看到,当延迟越高时,出错的概率就越大,会两个线程取到同一张票,或者造成票数-1被取。这是多个线程同时访问ticket变量所导致的。当一个线程开始执行时,判断是否有余票,有余票,对其减一,而另一个线程也检测到这张余票也对其减一。因为没有限制,且延迟存在的原因,导致两个线程同时对tacket操作,造成内存安全问题。
多线程安全问题的解决方案
而针对线程安全问题Java提供了四种解决方案
- synchronized锁(偏向锁,轻量级锁,重量级锁)
- volatile锁,只能保证线程之间的可见性,但不能保证数据的原子性
- jdk1.5并发包中提供的Atomic原子类
- Lock锁
并发的三大特性
- 原子性:原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
关键字
:synchronized。- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
关键字
:volatile、synchronized、final- 有序性:虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。
关键字
:volatile、synchronized
synchronized锁
synchronized锁是一种同步互斥锁,我们使用关键字Synchronized在某个代码段时,当执行到这段代码,检测到synchronized锁,就会把对象锁住。同一时间只能有一个线程去访问对象,从而保证我们的内存安全。
它的使用场景共有三种
- 修饰普通方法:它会给我们类的实例化对象加锁,每个实例化对象的锁互不干扰。
- 修饰静态方法:它会为我们的类Class加锁,所有实例化对象共享一把锁。
- 修饰代码块:它会为指定对象加锁,同一时间只能有一个线程访问该对象。
修饰普通方法
public class Test {
public static void main(String[] args) {
Runnable r1 = new MyRunnable();
Thread t1 = new Thread(r1, "窗口一");
Thread t2 = new Thread(r1, "窗口二");
Thread t3 = new Thread(r1, "窗口三");
Thread t4 = new Thread(r1, "窗口四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyRunnable implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if (ticket <= 0) {
break;
}
sale();
}
}
public synchronized void sale() {
System.out.println(Thread.currentThread().getName() + "卖出了一张票,票数为:" + ticket--);
}
}
修饰静态方法
而如果一个类是使用继承Thread的方式实现线程,每个线程都将创建一个实例化对象,那么修饰普通方法的方式将无法实现类资源的锁定,那么将需要使用修饰静态方法来实现。
public class Test {
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyThread extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
if (ticket <= 0) {
break;
}
sale();
}
}
public synchronized static void sale() {
System.out.println(Thread.currentThread().getName() + "卖出了一张票,票数为:" + ticket--);
}
}
修饰代码块
有时候我们不希望只所住我们本身的自己类,特别是在实际开发中,各种类对象之间的相互调用,而修饰代码块的方式则可以让我们锁住我们需要使用的对象。
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket(100);
MyRunnable r1 = new MyRunnable(ticket);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
Thread t3 = new Thread(r1);
Thread t4 = new Thread(r1);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket{
private int ticket;
public Ticket(int ticket){
this.ticket = ticket;
}
public int getTicket(){
return ticket;
}
public void sale(){
ticket--;
}
}
class MyRunnable implements Runnable {
private Ticket ticket;
public MyRunnable(Ticket ticket){
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
if (ticket.getTicket() <= 0) {
break;
}
// 锁住ticket对象
synchronized (ticket){
System.out.println(Thread.currentThread().getName()+":卖出的是"+ticket.getTicket());
ticket.sale();
}
}
}
}
锁实现原理
synchronized的实现依赖虚拟机,我们在深入探讨Synchronized之前先来看一下jvm里面的一个重要概念:
对象头:java中的对象在内存中存储的时候有一个重要的组成部分,就是对象头,对象头中主要包括两部分数据:类型指针和标记字段,通过类型指针可以知道该对象是什么类型的,标记字段用来存储对象运行时的数据,其中就有锁对象的指针,它是我们理解Synchronized的关键,因为Synchronized使用到了各种锁。
标记字段中的锁对象指针指向了一个monitor对象,每个对象都有一个对应的monitor对象,monitor对象是同步工具,线程在执行加了Synchronized的代码段的时候要先去获取对象的monitor,执行完毕释放monitor。此过程是互斥的,一次只能有一个线程获取monitor,只有该线程释放monitor以后其它线程才能再获取它。
偏向锁,轻量级锁与重量级锁
在java1.6之后Synchronized进行了一些优化,引入了偏向锁、轻量级锁、重量级锁
。
1. 偏向锁:在很多情况下,虽然我们加了Synchronized关键字,保证了它的线程安全性,但是在实际当中多数情况下只有一个线程运行这段代码,并没有其它线程来和它竞争。这一个线程每次执行代码都需要获取锁和释放锁这个开销是没有必要的。所以引入了偏向锁,偏向锁在一个线程第一次访问的时候将该线程的id记录下来,下次判断如果还是该线程就不会加锁了。如果有另一个线程也来访问它,说明有可能出现线程并发。此时偏向锁就会升级为轻量级锁。
2. 轻量级锁:如果有其它线程也来访问这段代码,偏向锁会升级为轻量级锁,此时的线程是交替执行被加锁的代码段,一旦另一个线程需要同时执行加锁的代码段,就会形成竞争,首先会自旋,一段时间后轻量级锁升级为重量级锁。
3.重量级锁:重量级锁也就是文章开始提到的通过monitor对象来实现的,我们先看下重量级锁的执行过程当一段代码被上锁以后同一时间只能允许一个线程(线程一)执行,此时如果有另一个线程(线程二)也要执行这段代码就会被阻塞,挂起,线程一执行完后,线程二还会被唤醒,这些动作都是操作系统级别的处理,很消耗资源。
在轻量级锁中提到过一个概念,自旋,我们来看下自旋是怎么回事,当线程二发现无法执行该代码的时候它并不会挂起而是开始自旋,也就是在那里等待,不断的尝试执行,该操作会消耗cpu.自旋因为线程不需要被挂起和唤醒,执行速度很快,但是此时cpu在不停的运转,所以吞吐量会较低。
volatile
volatile保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。因为volatile的开销有时比synchnized的开销少,所以我们很多时候在不需要满足原子性的场景使用volatile来保证线程安全,适合多个线程读,一个线程写的场合
。
栗子:
public class Test {
// 修饰变量,修改后立马更新
private static volatile int ticketNumber = 100;
public static void main(String[] args) {
Ticket ticket = new Ticket(ticketNumber);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始增加,目前为"+ticket.getTicket());
ticket.add();
}
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket.getTicket());
}
}
});
Thread t3 = new Thread(new Runnable(){
@Override
public void run() {
System.out.println(ticket.getTicket());
}
});
t1.start();
t2.start();
t3.start();
}
}
class Ticket {
private int ticket = 100;
public Ticket(int ticket){
this.ticket = ticket;
}
public int getTicket(){
return ticket;
}
public void add(){
ticket++;
}
}
Atomic原子类
Lock锁
在JDK1.5之后新增加了一种更强大的线程同步机制—通过显示定义同步锁来实现线程同步解决线程安全问题。同步锁使用Lock对象充当。
java.util.concurrent.locks.lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的单独访问,每一次只能有一个线程对Lock对象加锁,并且线程在访问共享资源之前应该先加锁。
ReentrantLock类实现了Lock,它拥有和synchronized相同的并发行和内存语义,在实现线程安全的控制中,比较常用的就是ReentrantLock,可以实现显示的加锁和释放锁,比较灵活,更加节省资源。同时也有ReentrantReadWriteLock这种乐观锁也较为常用,使用方法类似。
栗子
public class Test {
private static volatile int ticketNumber = 100;
public static void main(String[] args) {
MyRunnable r1 = new MyRunnable();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
Thread t3 = new Thread(r1);
Thread t4 = new Thread(r1);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyRunnable implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket <= 0) {
break;
}
lock.lock();
System.out.println(Thread.currentThread().getName()+":目前票数为"+ticket--);
lock.unlock();
}
}
}
线程并发协作经典案例
生产者-消费者模式
多线程环境下,往往需要多个线程能够并发协作,为了解决这个难题,引入一个重要的多线程并发协作模型——生产者-消费者模式。
该模型的核心就是利用缓冲区的概念解决并发问题。消费者和生产者并不直接产生联系。实现了线程之前的分离。
下面是实例代码:
public class TestPro {
public static void main(String[] args) {
SycStack stack = new SycStack();
Thread cost = new Thread(new cost(stack));
Thread pro = new Thread(new produce(stack));
cost.start();
pro.start();
}
}
class Mantou{
int id;
public Mantou(int id) {
this.id=id;
}
}
class SycStack {
int index = 0;
Mantou[] mt = new Mantou[10];
public synchronized void push(Mantou m) {
while (index == mt.length) {
try {
this.wait(); //等待方法可以将锁释放
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
this.notify(); //唤醒线程
mt[index]=m;
index++;
}
public synchronized Mantou pop() {
while (index==0) {
try {
this.wait(); //释放资源
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
this.notify(); //唤醒线程
index--;
return mt[index];
}
}
class produce implements Runnable{
SycStack stack = null;
public produce(SycStack c) {
this.stack=c;
}
@Override
public void run() {
// TODO 自动生成的方法存根
for(int i=1;i<10;i++) {
System.out.println("生产馒头:"+i);
Mantou m = new Mantou(i);
stack.push(m);
}
}
}
class cost implements Runnable{
SycStack stack = null;
public cost(SycStack c) {
this.stack=c;
}
@Override
public void run() {
// TODO 自动生成的方法存根
for(int i=1;i<10;i++) {
Mantou m = stack.pop();
System.out.println("消费馒头:"+i+" 这个馒头是-》"+m.id);
}
}
}
运行结果:
经典面试题
线程通信是什么,如何实现线程通信?
我只会简单使用一些线程的方法来实现线程通信,如join,wait(),notify()等,用这些做过简单的生产者消费者模式和一些其他的小栗子,线程通信的工作类没有涉及过。
你对死锁有了解吗,该怎么避免?
- 死锁问题是两个线程相互等待对方的锁释放,解决方法是一个同步块不要同时持有两个以上的对象锁。
sleep和wait方法有什么区别?
- sleep是线程类的方法,它是进入一定时间的睡眠状态,会释放CPU资源,但不释放锁,指定时间结束后自动进入就绪状态
- wait是Object的方法,它是将正在运行的线程进入一个等待池,会释放CPU资源和锁,需要有notify方法手动唤醒,唤醒后进入就绪状态。
拓展
乐观锁与悲观锁
-
悲观锁:像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态.
悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized -
乐观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态.
乐观锁认为竞争不总是会发生,因此它不需要持有锁,将”比较-替换”这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
悲观锁synchronized
我们使用的synchronized同步锁就是一种悲观锁,通过这种同步方式获得的锁也被称为互斥锁。它不允许不同线程同时操作持有相同互斥锁的资源。
乐观锁
乐观锁并不真正存在对资源的锁定,它只是一种保持数据一致性的另一种方式。就是它不对资源进行任何锁定,只有在提交更新的时候校验数据是否可以可以被写入。
- 为数据库增加一个字段version,当前版本为version1,操作员A对金额进行取款操作,操作原也进行取款操作。
- 操作员A提交(money-10)时,校验当前操作版本version1和数据库版本version1是否一致。一致,数据库版本更改为version2,金额更改。
- 操作员B提交(money-20)时,当前操作版本为version1,而数据库版本为version2,操作被驳回。
ReentrantReadWriteLock 读写锁。是一种原子粒度的乐观锁。
使用方式:
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);//新建一个锁
//操作共享资源时开启
lock.writeLock().lock();
//操作完后释放
lock.writeLock().unlock();
我们可以写一个简单的例子。
class MyRunnable1 implements Runnable {
private static int tickets = 100;
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
queryTickets();
lock.writeLock().lock();
if (tickets <= 0)
break;
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
lock.writeLock().unlock();
}
}
public void queryTickets(){
System.out.println("tickets余量为:"+tickets);
}
}