引言
如何理解多线程呢,目前我们写的程序都单线程,单线程指的是整个程序只有一条唯一执行路径 所有代码被串联到这条路径中。多线程指的就是一个程序中,存在类似多条执行路径。
一、多线程基础
程序、进程、线程
程序
- 是为完成特定任务,用某种语言编写的一组指令的集合,是一段静态代码
进程
- 是程序的一次执行过程,正在运行的一个程序。进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。
线程
- 线程是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如
主线程
、gc线程
main()
称之为主线程,为系统的入口,用于执行整个程序- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不是人为可干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如
cpu
调度时间,并发控制开销 - 每个现场在自己的工作内存交互,内存控制不当会造成数据不一样
二、线程创建[高频面试]
继承Thread类
将类声明为**Thread**
的子类。该子类**重写**
Thread类的**run**
方法。创建对象,开启线程。
Thread类的一些方法:public final String getName()
获取线程名称public final void setName(String name)
指定线程名称public static Thread currentThread()
获取当前线程对象
开启线程的步骤:
1、定义一个**Thread类的子类**
,**重写run方法**
2、创建自定义的线程子类对象
3、开启线程动作
**public void start() **
使该线程开始执行
自动定义线程类
//线程类
class MyThread extends Thread{
}
重写run()
//线程类
public class MyThread extends Thread {
//重写run , 这个线程要做的事情
@Override
public void run(){
for (int i = 1; i < 10; i++) {
System.out.println("MyThread:"+i);
}
}
}
创建并启动线程
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
//返回main线程
Thread mainThread = Thread.currentThread();
for (int i = 1; i < 10; i++) {
System.out.println(mainThread.getName()+":"+i);
}
}
}
启动线程 必须使用 **start() **
,不可直接调用run()
, 区别在于 调用start() 是触发JVM调用run() 为当前对象对象分配自己的栈空间,形成一条线程。 直接调用run() 则不会,就跟平时一样相当于掉一个普通方法
习题:购买火车票
public class BuyTicketThread extends Thread{
//设置线程名字
public BuyTicketThread(String name){
super(name);
}
//多个对象共享10张票
static int ticketNum = 10;
@Override
public void run() {
//每个窗口有100个人在排队
for (int i = 1; i < 100; i++) {
if(ticketNum > 0){ //判断,票数大于0开始抢票
System.out.println("我在"+this.getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
}
}
}
}
public class Test {
public static void main(String[] args) {
//多个窗口抢票:三个窗口三个线程对象
BuyTicketThread t1 = new BuyTicketThread("窗口1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("窗口2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("窗口3");
t3.start();
}
}
实现Runnable接口
Thread
类本身就是Runnable
的实现类,它还定义了一些管理线程的方法。
使用Runnable方式创建线程,依然要依赖Thread。Runnable 仅仅表达的是线程要执行的任务。使用这种方式的思想,希望把线程对象 与线程要执行的任务分离开来。
开启线程的步骤:
1、定义Runnable
线程执行目标实现类,重写**run**
方法
2、通过指定线程执行目标的构造方法创建线程对象
a) 创建线程执行目标对象
b) 通过线程执行目标创建线程对象
3、开启线程动作 (start开启线程)
创建Runnable实现类,重写
**run**
方法
public class MyRunnable implements Runnable{
@Override
public void run() {
//返回当前线程
Thread thread = Thread.currentThread();
for (int i = 1; i <10 ; i++) {
System.out.println(thread.getName()+":"+i);
}
}
}
创建线程并启动(通过指定线程执行目标的构造方法创建线程对象并开启线程)
public class RunnableDemo {
public static void main(String[] args) {
//创建线程执行目标
MyRunnable mr = new MyRunnable();
//通过指定线程执行目标的构造方法创建线程对象
Thread thread = new Thread(mr);
thread.setName("Hi");
//开启线程动作
thread.start();
//返回main线程
Thread mainThread = Thread.currentThread();
for (int i = 1; i < 10; i++) {
System.out.println(mainThread.getName()+":"+i);
}
}
}
习题:买火车票
public class BuyTicketThread implements Runnable{
//多个对象共享10张票
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i < 100; i++) {
if(ticketNum > 0){
System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
}
}
}
}
public class Test {
public static void main(String[] args) {
//定义线程对象
BuyTicketThread t = new BuyTicketThread();
//窗口1买票
Thread t1 = new Thread(t, "窗口1");
t1.start();
//窗口2买票
Thread t2 = new Thread(t, "窗口2");
t2.start();
//窗口1买票
Thread t3 = new Thread(t, "窗口3");
t3.start();
}
}
两种实现方式对比
方式一,线程对象和线程任务是耦合的,方式二,线程对象和线程任务是分离的
方法一,扩展性不好,类单继承
的。 方式二,扩展性更好,接口是多实现
。共享资源能力强,不需要加**static**
修饰
方法一,调用线程方法直接 ,方式二,不可直接调用线程方法,需要先调用 **Thread._currentThread_()**
,这个方法的作用是拿到与当前线程任务绑定的线程对象。
实现Callable接口
- 是在JDK1.5之后出现的。
优点:①有返回值 ②能抛出异常
缺点:创建线程比较麻烦
public class TestRandomNum implements Callable<Integer> {
/*
1、实现Callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值是Object类型
2、如果带泛型,那么call的返回值就是泛型对应的类型
3、从call方法看到,方法有返回值,可以抛出异常
*/
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);//返回10以内的随机数
}
}
class Test{
public static void main(String[] args) throws ExecutionException,InterruptedException {
//定义一个线程对象
TestRandomNum trn = new TestRandomNum();
FutureTask ft = new FutureTask(trn);
Thread t = new Thread(ft);
t.start();
//获取线程得到的返回值
Object o = ft.get();
System.out.println(o);
}
}
三、线程的生命周期
线程的生命周期包括**5**
个阶段,**新建**
、**就绪**
、**运行**
、**阻塞**
、**销毁**
- 新建:使用
**new**
关键字创建一个线程对象,这个阶段仅仅是在**JVM**
中开辟一个内存空间; - 就绪:调用线程对象的
**.start方法()**
,使线程处于**runnable**
可运行状态; - 运行:
**CPU**
分配时间片给线程,线程开始执行run
方法里面定义的各种操作; - 阻塞:线程在运行状态的时候,可能会遇到一些特殊情况,导致线程停止下来,如
**sleep()**
,**wait()**
,处于阻塞状态的线程需要等待其他机制使得线程的阻塞状态被唤醒,比如调用**notify()**
,**notifyall()**
。被唤醒的线程不会立即进行执行run方法的操作,而是需要等待CPU重新分配时间片进行执行; - 销毁:线程执行完毕,或者线程被终止、或者线程里面执行的方法出现异常,就会导致线程被销毁,释放资源;
线程生命周期图
线程生命周期状态流转详解
我们字节码执行引擎执行一下代码
Thead thread = new Thread();
这时间线程的创建,属于JVM中新增一个对象,给对象分配内存空间,并不是指操作系统创建了一个线程,接着上面的代码,我们调用thread
对象的**start()**
方法时候,线程才会被创建出来,进入就绪runnable状态,这是一个可运行状态,等待CPU分配时间片。
thread.start();
当线程处于Runnable状态后,会有什么样的变化呢?
线程处于Runnable
顾名思义,线程此时处于一种可以被运行状态,当分配到CPU时间片后,那么就会进入运行状态
反过来思考,Runnable状态可否进入Blocked状态或者Terminated状态吗?
这明显是不行的,Runnable只是一种可运行状态,只有真正运行中的线程,才可以被阻塞或者被终止,一个线程都没有真正被执行,怎么会进入被阻塞或者终止呢?
当线程分配到时间片后,进入运行Running状态;
当线程处于Running状态后,会有什么变化呢?
正在运行的线程,如果顺利执行完任务之后,那么任务结束,线程就应该被销毁,面得占用CPU资源,因此很容易想到线程会到达Terminated状态
正在运行的线程,如果中途需要停止下来那也是可以的,这个时候线程就会被阻塞起来,一直停留在某个阶段
正在运行的线程,如果运行中,出现系统异常,这样就会导致线程直接进入Terminated状态
正在运行的线程,我们也可以将其直接返回就绪状态,比如调用yield方法
当线程处于Blocked状态后会出现什么情况呢?
被阻塞的线程,自然会有被唤醒时,但是被唤醒的线程,不能直接进入Running状态,而是先进入Runnable状态,等待CPU重新分配时间片
可以思考一下,如果Blocked中的线程直接进入Running状态吗?不可能的,因为线程时靠CPU分配时间片来运行的,CPU没有分配时间片,线程是没有权限跑起来的,因此,必须回到Runnable状态,然后等待CPU分配时间片。
同时,在阻塞状态的线程也可以通过interrupted 方法进行中断
使线程处于阻塞状态的方法有sleep(),wait(),这两者有什么区别呢?
使用sleep()方法,使线程处于睡眠状态,等待指定时间就会运行,如果代码块被加了锁,则线程不释放锁;
使用wait()方法,使线程处于阻塞状态的时候,wait()方法自动释放了锁,而且wait()方法必须使用notify()或者notifyAll()方法才能唤醒线程
sleep(1000)代表在阻塞1s,1s内部释放锁,也不会分配到CPU时间片,过了1s后进入runnable状态,重新等待执行时间片
wait(1000),占用锁1s中,过后1s后释放锁,但是还是出于阻塞状态,直到notify()/notifyAll()
四、线程常见方法
start()
启动线程 | |
---|---|
**run()** |
线程类,继承**Thread** 类或者实现**Runnable** 接口的时候,都需要实现这个run()方法,run()方法里面是线程要执行的内容 |
currentThread |
**Thread** 类中一个静态方法,获取当前正在执行的线程 |
setName()/getName() |
取名字/获得名字,如果没有设置名字,默认名字为Thread-(0-n) |
setPriority(int xx)/getPriority() |
设置/获取优先级 ,默认三个 MIN_PRIORITY = 1;NORM_PRIORITY = 5;MAX_PRIORITY = 10; |
join() |
同步,加入线程线程中 注意:必须先start,再join才有效 |
Thread.sleep( long ms ) |
睡眠 暂停执行,直到睡眠时间到 |
setDeamon(boolean xx)/isDeamon() |
设置伴随进程:将子线程设置为主线程的伴随进程,主线程停止的时候,子线程也不要继续执行了(先设置,再启动) |
Thread.yield() |
线程礼让. 让出cpu执行时间片,自己进入就绪状态,再次等待调度 |
stop() |
五、线程安全问题[重点]
什么时候会出现线程安全问题
在单线程中不会出现线程安全问题
,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会导致共享的资源出现问题,一个线程还没执行完,另一个线程就参与进来了,开始争抢
。
购买火车票出现问题:**重票,错票**
出现2个或者3个10张票
线程1: 我在窗口1买到了广州到北京的第10张火车票,还没等`--`操作,就被线程2抢走了资源<br /> 线程2: 我在窗口2买到了广州到北京的第10张火车票,还没等`--`操作,就被线程3抢走了资源<br /> 线程3: 我在窗口2买到了广州到北京的第10张火车票,这个时候执行了`--`操作,则票数为`**9**`
出现0,-1,-2张票
线程1: 我在窗口1买到了广州到北京的第1张火车票,还没等`--`操作,就被线程2抢走了资源<br /> 线程2: 我在窗口2买到了广州到北京的第1张火车票,就被线程3抢走了资源<br /> 线程3: 我在窗口2买到了广州到北京的第1张火车票,就被线程1抢走了资源<br /> 线程1: 执行`--`操作,票数为`**0**`<br /> 线程2: 执行`--`操作,票数为`**-1**`
如何解决线程安全问题
方法1: 同步代码块
Java中使用
**synchronized(obj)**
关键字来解决,将一个完整动作使用synchronized包裹
。原理当线程执行到有synchronized
的代码块时,先尝试去获取obj 对象的锁(每个对象有且只有一把),如果取到了则进入代码块中执行,期间不被打扰(如果这时有其他线程准备进入代码块但是取不到锁),直到执行完毕后,自动归还锁。其他线程方可进入。同步代码块中的锁对象可以是任意的对象;但
多个线程操作相同数据时
,要使用同一个锁对象才能够保证线程安全,避免安全问题
。synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块演示1:
synchronized (this)
:this指调用当前对象
,相当于调用一个t
对象。
//窗口线程
public class BuyTicketThread implements Runnable{
//多个对象共享10张票
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i < 100; i++) {
synchronized (this) {//把具有安全隐患的代码锁住即可
if (ticketNum > 0) {
System.out.println("我在" + Thread.currentThread().getName() + "买到了广州到北京的第" + ticketNum-- + "张车票");
}
}
}
}}
//同步案例
public class Test01 {
public static void main(String[] args) {
//定义线程对象
BuyTicketThread t = new BuyTicketThread();
//窗口1买票
Thread t1 = new Thread(t, "窗口1");
t1.start();
//窗口2买票
Thread t2 = new Thread(t, "窗口1");
t2.start();
//窗口1买票
Thread t3 = new Thread(t, "窗口1");
t3.start();
}
}
同步代码块演示2:
synchronized(**BuyTicketThread.class**)
小括号**()**
里需要**一个对象**
,这个对对象必须满足,不同线程进入时是同一个对象。这样才有互斥性(多个线程用同一把锁)
public class BuyTicketThread extends Thread {
//设置线程名字
public BuyTicketThread(String name){
super(name);
}
//多个对象共享10张票
static int ticketNum = 10;
@Override
public void run() {
//每个窗口有100个人在排队
for (int i = 1; i < 100; i++) {
synchronized (BuyTicketThread.class) {//多个线程用同一把锁
if (ticketNum > 0) { //判断,票数大于0开始抢票
System.out.println("我在" + this.getName() + "买到了广州到北京的第" + ticketNum-- + "张车票");
}
}
}
}}
public class Test {
public static void main(String[] args) {
//多个窗口抢票:三个窗口三个线程对象
BuyTicketThread t1 = new BuyTicketThread("窗口1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("窗口2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("窗口3");
t3.start();
}
}
同步监视器总结
总结1:认识同步监视器(锁子)——>synchronized
(同步监视器)
- 必须是
**引用数据类型**
,不能是基本数据类型 - 可创建一个专门的同步监视器,没有任何业务含义
- 一般使用
共享资源
做同步监视器即可 - 在同步代码块中不能改变同步监视器对象的引用
- 尽量不要使用
String
和包装类Integer
做同步监视器 - 建议使用
final
修饰同步监视器
总结2:同步代码块的执行过程
- 第一个线程来到同步代码块,发现同步监视器
open
状态,需要close
,然后执行其中的代码 - 第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了
cpu
,但是没有开锁open
- 第二个线程获取了
cpu
,来到了同步代码块,发现同步监视器close
状态,无法执行其中的代码,第二个线程也进入阻塞状态 - 第一个线程再次获取
cpu
,接着执行后续代码,同步代码块执行完毕,释放锁open
第二个线程也再次获取
cpu
,来到了同步代码块,发现同步监视器open
状态,拿到锁并上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)强调:同步代码块中能发生`**cpu**`的切换吗,能!但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧`close`)
总结3:其他
- 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其他的任何一个代码块
- 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
方法2: 同步方法
同步方法演示1:
public class BuyTicketThread implements Runnable{
//多个对象共享10张票
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i < 100; i++) {
buyTicket();
}
}
public synchronized void buyTicket(){//把具有安全隐患的代码锁住即可
if(ticketNum > 0){
System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
}
}
}
public class Test {
public static void main(String[] args) {
//定义线程对象
BuyTicketThread t = new BuyTicketThread();
//窗口1买票
Thread t1 = new Thread(t, "窗口1");
t1.start();
//窗口2买票
Thread t2 = new Thread(t, "窗口2");
t2.start();
//窗口1买票
Thread t3 = new Thread(t, "窗口3");
t3.start();
}
}
同步方法演示1:
public class BuyTicketThread extends Thread{
//设置线程名字
public BuyTicketThread(String name){
super(name);
}
//多个对象共享10张票
static int ticketNum = 10;
@Override
public void run() {
//每个窗口有100个人在排队
for (int i = 1; i < 100; i++) {
buyTicket();
}
}
public static synchronized void buyTicket(){
if(ticketNum > 0){ //判断,票数大于0开始抢票
System.out.println("我在"+Thread.currentThread().getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
}
}
}
public class Test {
public static void main(String[] args) {
//多个窗口抢票:三个窗口三个线程对象
BuyTicketThread t1 = new BuyTicketThread("窗口1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("窗口2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("窗口3");
t3.start();
}
}
总结:
- 不要将
run()
定义为同步方法,效率太低 - 非静态同步方法的同步监视器是
this
,静态同步方法的同步监视器是类名.class
字节码信息对象 - 同步代码块的效率要高于同步方法(原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部)
- 同步方法的锁是
this
,一旦锁住一个方法,就锁住了所有的同步方法,同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
方法3: Lock锁
JDK1.5
后新增的- 与采用
synchronized
相比,Lock
可提供多种锁方案,更灵活 synchronized
是java中的关键字,是靠jvm
来识别完成的,是虚拟机级别的Lock
锁上API
级别的,提供了相应的借口和对应的实现类,更灵活,表现出来的性能优于之前的方式
�
代码演示
public class BuyTicketThread extends Thread{
//设置线程名字
public BuyTicketThread(String name){
super(name);
}
//多个对象共享10张票
static int ticketNum = 10;
//拿来一把锁
Lock lock =new ReentrantLock();
@Override
public void run() {
//每个窗口有100个人在排队
for (int i = 1; i < 100; i++) {
//打开锁
lock.lock();
try {
if(ticketNum > 0){ //判断,票数大于0开始抢票
System.out.println("我在"+this.getName() +"买到了广州到北京的第"+ticketNum-- +"张车票");
}
}catch(Exception e){
e.printStackTrace();
}finally {
//关闭锁:即使有异常,这个锁也可以得到释放
lock.unlock();
}
}
}}
Lock和synchronized的区别
- Lock
显式锁
(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁
- Lock只有
代码块锁
,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将话费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
�优先使用顺序
Lock—->同步代码块(已经进入了方法体,分配了相应资源)—->同步方法(在方法体之外)
死锁问题[高频面试]
- 多个线程因为竞争资源而陷入相互等待的情况,无法恢复的场景。
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程—资源的环形链。
public class TestDeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(),o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
//当flag = 1锁住o1
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
//只要锁住o2就完成
synchronized (o2) {
System.out.println("2");
}
}
}
//如果flag==0锁住o2
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
//只要锁住o2就完成
synchronized (o1) {
System.out.println("3");
}
}
}
}
public static void main(String[] args) {
//实例2个线程累
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 1;
td2.flag = 0;
//开启2个线程
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.start();
t2.start();
}
}
预防死锁:
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)解决方法:减少同步资源的定义,避免嵌套同步
六、线程通信问题
什么是线程通信
协调多个线程有序访问某些资源。
原理
在java对象中,有两种池
锁池:synchronized
等待池:wait()
、notify()
、notifyAll()
如果一个线程调用了某个对象的wait()
方法,那么该线程进入到该对象的等待池中(并且已经将锁释放)
如果未来的某一时刻,另外一个现场调用了相同对象的notify
方法或者notifyAll
方法,那么该等待池中的线程就会被唤起,然后进入到对象的锁池里面去获得该对象的锁,如果获得锁成功,那么该线程就会向wait
方法之后的路径继续执行,注意是向wait
方法之后
线程通信方法
wait()
: 它是Object的方法,不是线程提供的方法,让执行这个wait()调用的线程等待。 无限期等待,直到有被唤醒。wait( long ms )
: 等待指定的毫米数,如果没有被唤醒,则自动唤醒wait( long ms, int naos)
: 更精确的等待时间notify()
: 唤醒等待在该对象上的线程(只会唤醒一个)。notifyAll()
: 唤醒全部等待在该对象上的全部线程。
这些方法必须使用在存在 synchronized 代码块内 或者 synchronized 方法中。
package waitandnofity;
/**
* 等待唤醒案例
*/
public class WaitAndNotifyCase {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread son1 = new Thread( new Foo(obj) );
son1.setName("张三");
Thread son2 = new Thread( new Foo(obj) );
son2.setName("李四");
son1.start();
son2.start();
//让主线程等5s后去唤醒其他线程
Thread.sleep(5000);
synchronized (obj){
//obj.notify();//唤醒一个
obj.notifyAll();//唤醒全部
}
}
}
class Foo implements Runnable{
Object obj = null;
public Foo(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj){
System.out.println(Thread.currentThread().getName()+ "执行开始...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "执行完毕...");
}
}
}
生产者消费者模型
package producerconsumer;
/**
* 生产者和消费者案例
*/
public class ProducerAndConsumer {
public static void main(String[] args) {
//容器
Container container = new Container();
Thread xfz1 = new Thread( new Consumer(container) );
xfz1.setName("张三");
xfz1.start();
Thread xfz2 = new Thread( new Consumer(container) );
xfz2.setName("李四");
xfz2.start();
Thread scz1 = new Thread( new Producer( container ) );
scz1.setName("王师傅");
scz1.start();
Thread scz2 = new Thread( new Producer( container ) );
scz2.setName("李师傅");
scz2.start();
}
}
//抽象出一个容器事物
class Container{
Object[] data = new Object[10]; //底层使用数组模拟栈
int size; //元素计数器
//存
public synchronized void add( Object obj ){
while( size==data.length ){ //存满了,不能存了,生产者线程,等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data[size++] = obj;
System.out.println(Thread.currentThread().getName() + "生产"+obj);
//唤醒刚刚因为没有商品而等待的消费者线程
this.notifyAll();
}
//取
public synchronized Object get(){
while (size==0){
try {
this.wait(); //商品不足 消费者线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object result = data[--size];
System.out.println(Thread.currentThread().getName() +"消费"+result);
//唤醒刚刚因为没有空间 而等待的生产者线程
this.notifyAll();
return result;
}
}
//生产者
class Producer implements Runnable{
//共享容器
Container container ;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for ( int i =1;i<=20; i++ ){
container.add( "汉堡"+i);
}
}
}
//消费者
class Consumer implements Runnable{
//共享容器
Container container ;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
for ( int i =1;i<=20; i++ ){
container.get();
}
}
}
sleep和wait的区别
- sleep进入阻塞状态没有释放锁,wait进入阻塞状态且同时释放锁
七.JUC锁[高频面试]
锁分类
锁的作用就是保证数据一致性,但是往往就会牺牲效率,所以使用锁需要自己做好平衡和取舍。同等情况下,当然使用性能更好的锁,是最好的方案。
悲观锁
怀疑任何情况都会出现并发问题,所以在设计的时候 就默认锁定,jdk1.5前就是 synchronized ,早期说这个性能有问题(经过优化现在一般不去比较了)。在JDK1.5新的解决方法是提供了Lock接口和他的实现类。例如 ReentrantLock ReentrantReadWriteLock等。
乐观锁
乐观锁就是和悲观锁相反的思想,就是先不怀疑存在并发问题,如果确有存在再解决,通常需要为并发修改的数据设定一个版本号,修改前对一下先前获取的版本号,修改时在比较版本号如果一致才修改,不一致说明有版本变化不可修改。CAS算法就是一种乐观锁算法,它是硬件层面实现的,把多语句指令和为一个指令,确保了原子操作。
Compare And Swap( 比较 和 交换 ) V (内存值) E(期望值) N(新值) 当且仅当 V==E时 才将 V=N 。
ReentrantLock
JDK1.5 引入全新的锁体系,JUC包大部分都是这个版本引入的。ReentrantLock是Lock接口的实现类,意为重入锁(所谓重入锁就是允许同一个线程多次对对象去锁),具有和synchronized ,还提供了一些方法。功能更强大
package lock.synchronize.block;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//同步案例
public class TicketsDemo {
public static void main(String[] args) {
//模拟3个窗口
Thread win1 = new Thread( new Window() );
win1.setName("张阿姨");
win1.start();
Thread win2 = new Thread( new Window() );
win2.setName("王阿姨");
win2.start();
Thread win3 = new Thread( new Window() );
win3.setName("李阿姨");
win3.start();
}
}
//窗口线程
class Window implements Runnable{
//声明锁
static Lock lock = new ReentrantLock(true);
//模拟100张票
static int total = 100;
@Override
public void run() {
while (true){
try{
lock.lock(); //获得锁
if(total>0) {
total--;
System.out.println(Thread.currentThread().getName() + "卖出1张,还剩" + total);
}else{
break;
}
}finally {
lock.unlock(); //确保一定可以解锁
}
}
}
}
和对比 synchronized
- synchronized 关键字 ReentrantLock 类(jdk1.5新引入)
- 都是重入锁(ReentrantLock 功能强大 提供了更多的方法,以及实现公平锁策略)
- synchronized 上锁与解锁自动完成 , ReentrantLock 需要自己上锁和解锁。
ReadWriteLock
凡是用锁都会影响效率,如果把这种影响做到最小了呢? 读写锁就是一种经过细化考量的一种优化的锁。这种锁的特点是 读-读不阻塞线程, 读-写要阻塞线程,写-写要阻塞。一个把读写锁可以分离出 读锁和写锁。
package lock.readwriteLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁案例
*/
public class TestReadWriteLockCase {
public static void main(String[] args) {
long begin = System.currentTimeMillis();
//并发访问源
Foo obj = new Foo();
//线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//提交任务 18 次读
for(int i= 0; i<18; i++){
executorService.submit(new Runnable() {
@Override
public void run() {
obj.getAge();
}
});
}
//提交2次写任务
for(int i=0;i<2;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
obj.setAge( 100 );
}
});
}
//关闭
executorService.shutdown();
while ( !executorService.isTerminated() ){
}
long end = System.currentTimeMillis();
//计算时间
System.out.println( (end-begin)/1000 );
}
}
class Foo {
private Integer age ;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public Integer getAge() {
try {
readWriteLock.readLock().lock();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
return age;
}
public void setAge(Integer age) {
try {
readWriteLock.writeLock().lock();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
this.age = age;
}
}
Condition 通信
在jdk1.5以后,如果不使用 synchronized 同步方法, 那么线程通信可以使用Condition 实现,它的核心做法是,
- 获得一个 阻塞条件对象 Conditon cd = lock.newCondition();
- 阻塞方法 cd.awiat() 作用同 wait()
唤醒方法 cd.signal() / cd.signalAll() 作用同 notify() 和 notifyAll()
```java package lock.producerconsumer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /**生产者和消费者案例 */ public class ProducerAndConsumer { public static void main(String[] args) { //容器 Container container = new Container(); Thread xfz1 = new Thread( new Consumer(container) ); xfz1.setName(“张三”); xfz1.start();
Thread scz1 = new Thread( new Producer( container ) ); scz1.setName(“王师傅”); scz1.start(); } }
//抽象出一个容器事物 class Container{ private Object[] data = new Object[10]; //底层使用数组模拟栈 private int size; //元素计数器 private ReentrantLock lock = new ReentrantLock(); //从锁上获取阻塞条件 private Condition cd = lock.newCondition(); //存 public void add( Object obj ){
try{
lock.lock();
while( size==data.length ){ //存满了,不能存了,生产者线程,等待
try {
cd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data[size++] = obj;
System.out.println(Thread.currentThread().getName() + "生产"+obj);
//唤醒刚刚因为没有商品而等待的消费者线程
//TODO
cd.signalAll();
}finally {
lock.unlock();
}
}
//取
public Object get(){
try{
lock.lock();
while (size==0){
try {
cd.await(); ; //商品不足 消费者线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object result = data[--size];
System.out.println(Thread.currentThread().getName() +"消费"+result);
//唤醒刚刚因为没有空间 而等待的生产者线程
cd.signalAll();
return result;
}finally {
lock.unlock();
}
}
} //生产者 class Producer implements Runnable{ //共享容器 Container container ;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for ( int i =1;i<=20; i++ ){
container.add( "汉堡"+i);
}
}
} //消费者 class Consumer implements Runnable{ //共享容器 Container container ; public Consumer(Container container) { this.container = container; } @Override public void run() { for ( int i =1;i<=20; i++ ){ container.get(); } } } ```