多线程
一,什么是多线程?
进程:一个正在执行的程序,比如说微信
线程:进程中的一个独立控制单元,线程控制着进程的执行
并行与并发:
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
二,为什么使用多线程编程?
①、为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
②、进程之间不能共享数据,线程可以;
③、系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
④、Java语言内置了多线程功能支持,简化了java多线程编程。
三,线程的生命周期?
- 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
- 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
- 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
- 等待/阻塞/睡眠 :在一个线程执行了sleep(不会释放锁)(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
-
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态,释放锁,调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),使线程回到可运行状态(Runnable)
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。
monitor
java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。
wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。
当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。
用法:
synchronized单独使用:
- 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容
-
public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } } }
- 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。
-
public class Thread1 implements Runnable { public synchronized void run() { ..do something } }
- synchronized, wait, notify结合:典型场景生产者消费者问题
-
/** * 生产者生产出来的产品交给店员 */ public synchronized void produce() { if(this.product >= MAX_PRODUCT) { try { wait(); System.out.println("产品已满,请稍候再生产"); } catch(InterruptedException e) { e.printStackTrace(); } return; } this.product++; System.out.println("生产者生产第" + this.product + "个产品."); notifyAll(); //通知等待区的消费者可以取出产品了 } /** * 消费者从店员取产品 */ public synchronized void consume() { if(this.product <= MIN_PRODUCT) { try { wait(); System.out.println("缺货,稍候再取"); } catch (InterruptedException e) { e.printStackTrace(); } return; } System.out.println("消费者取走了第" + this.product + "个产品."); this.product--; notifyAll(); //通知等待去的生产者可以生产产品了 }
volatile
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
- 针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的或者网络延迟,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能
四,创建线程
1、继承Thread类:
步骤:①、定义类继承Thread;
public class ThreadDemo1 { public static void main(String[] args) { //创建两个线程 ThreadDemo td = new ThreadDemo("zhangsan"); ThreadDemo tt = new ThreadDemo("lisi"); //执行多线程特有方法,如果使用td.run();也会执行,但会以单线程方式执行。 td.start(); tt.start(); //主线程 for (int i = 0; i < 5; i++) { System.out.println("main" + ":run" + i); } } } //继承Thread类 class ThreadDemo extends Thread{ //设置线程名称 ThreadDemo(String name){ super(name); } //重写run方法。 public void run(){ for(int i = 0; i < 5; i++){ System.out.println(this.getName() + ":run" + i); //currentThread() 获取当前线程对象(静态)。 getName() 获取线程名称。 } } }
2、实现Runnable接口: 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。
实现步骤: ①、定义类实现Runnable接口
②、覆盖Runnable接口中的run方法
将线程要运行的代码放在该run方法中。
③、通过Thread类建立线程对象。
④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象
⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
public class RunnableDemo { public static void main(String[] args) { RunTest rt = new RunTest(); //建立线程对象 Thread t1 = new Thread(rt); Thread t2 = new Thread(rt); //开启线程并调用run方法。 t1.start(); t2.start(); } } //定义类实现Runnable接口 class RunTest implements Runnable{ private int tick = 10; //覆盖Runnable接口中的run方法,并将线程要运行的代码放在该run方法中。 public void run(){ while (true) { if(tick > 0){ System.out.println(Thread.currentThread().getName() + "..." + tick--); } } } }
两种方法对比:
继承Thread:线程代码存放在Thread子类run方法中。
优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
劣势:已经继承了Thread类,无法再继承其他类。
实现Runnable:线程代码存放在接口的子类的run方法中。
优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
五,常用API
currentThread() 获取当前线程,如果在main方法中,则获取的是main,而不是被start()调用的方法
isAlive() 判断当前线程是否处于活跃状态,线程处于正在运行状态,或者准备开始运行的状态,都认为是活跃状态
sleep() 让当前正在执行的
yield() 该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。
interrupt() 虚拟机会在此线程上标记一个标志(这个中断标志只是一个布尔类型的变量),代表这个线程可能被中断,在后面的中断操作也是根据这个中断标志执行的,如果一个线程被标记了中断标识(调用 interrupt方法),然后调用sleep,wait,jion,io操作等,进入阻塞状态时,会抛出InterruptedException异常,然后清除标识位的中断标记
interrupted():测试当前线程是否中断。 该方法可以清除线程的中断状态 。
isInterrupted():测试这个线程是否被中断。线程的中断状态不受此方法的影响。
六,线程间通信
wait()
- wait( ) 使当前线程执行代码的线程进行等待,wait()方法时object类的方法,在wait()所在代码行处停止运行,直到接到通知或中断为止。在调用wait()方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用此方法,在调用wait()方法后,线程释放锁。在wait()方法返回前,线程与其他线程竞争重新获取锁,如果调用wait()方法时,没有持有适当的锁,,则抛出IllegalMonitorStateException异常
- 线程呈wait()状态时,调用线程对象的interrupt()方法会出现InturreputException异常
- 当多个执行 相同任务的线程,条件判断时wait(),用while(),不能用if, 当线程wait后,又被唤醒时,是从wait后main开始执行,如果用if,就不会执行条件判断了
notify()
notify()也必须在同步方法或同步块中调用,即在调用前,线程必须获取该对象的级别锁,该方法用于唤醒处于wait状态的线程,此方法执行后,不会释放锁,只有在在notify所在方法块执行完才会释放锁
join()
当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。
两个队列:
就绪队列
- 存储即将获得锁的线程
- 被notify并且拿到锁的线程
阻塞队列:
- 被阻塞的线程
- 调用wait的线程
Threadlocal类:
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
七,Lock类
相关API
getHoldCount()
方法来获取当前线程的锁定个数,所谓锁定个数就是当前线程调用lock方法的次数。
getQueueLength()
方法来得到等待锁释放的线程的个数。
hasQueuedThread(Thread thread)
查询该Thread是否等待该lock对象的释放。
hasQueuedThreads()
查询是否有线程等待获取此锁定
isLocked()
Java提供了简单判断一个锁是不是被一个线程持有,
isHeldByCurrentThread()
, 判断当前线程是否有此锁定。
lockInterruptibly()
也可以实现加锁,但是当线程被中断的时候,就会加锁失败,进行异常处理
tryLock()
方法来进行尝试加锁,只有该锁未被其他线程持有的基础上,才会成功加锁。
isFair 查看这个锁是否公平
lock: 在java.util.concurrent包内。共有三个实现:
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。
ReentrantLock的用法
Lock类的用法也是这样,通过Lock对象lock,用lock.lock
来加锁,用lock.unlock
来释放锁。在两者中间放置需要同步处理的代码。
public class MyConditionService { private Lock lock = new ReentrantLock(); public void testMethod(){ lock.lock(); for (int i = 0 ;i < 5;i++){ System.out.println("ThreadName = " + Thread.currentThread().getName() + (" " + (i + 1))); } lock.unlock(); } }
公平锁与非公平锁
公平锁:按照线程加锁的顺序来获取锁,即先来先得
非公平锁:随机竞争来得到锁
Condition的用法:
Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的,但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。
下面,看一个例子,显示简单的等待/通知
public class ConditionWaitNotifyService { private Lock lock = new ReentrantLock();//创建锁 public Condition condition = lock.newCondition();//创建condition实例 public void await(){ try{ lock.lock();//开启锁 System.out.println("await的时间为 " + System.currentTimeMillis()); condition.await(); System.out.println("await结束的时间" + System.currentTimeMillis()); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock();//释放锁 } } public void signal(){ try{ lock.lock(); System.out.println("sign的时间为" + System.currentTimeMillis()); condition.signal(); }finally { lock.unlock(); } } }
- condition对象通过
lock.newCondition()
来创建,用condition.await()
来实现让线程等待,是线程进入阻塞。 - 用
condition.signal()
来实现唤醒线程。唤醒的线程是用同一个conditon对象调用await()
方法而进入阻塞。 - await()和signal()也是在同步代码区内执行。
读写锁ReentrantReadWriteLock
:
读写锁分成两个锁,一个锁是读锁,一个锁是写锁。读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的。
看下面的读读共享的例子:
public class ReadReadService { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void read(){ try{ try{ lock.readLock().lock(); System.out.println("获得读锁" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); Thread.sleep(1000 * 10); }finally { lock.readLock().unlock(); } }catch (InterruptedException e){ e.printStackTrace(); } } }
测试的代码和结果如下:
ReadReadService service = new ReadReadService(); Thread a = new Thread(service::read); a.setName("A"); Thread b = new Thread(service::read); b.setName("B"); a.start(); b.start();
八,定时器
Timer
作用是设置计划任务,而封装任务内容的类是TimerTask类.此类是一个抽象类,继承需要实现一个run方法.
package com.wang.reflect; import java.util.Timer; import java.util.TimerTask; class MyTask extends TimerTask{ @Override public void run() { System.out.println("您该起床了!!!!"); } } public class TimerDemo { public static void main(String[] args) { //创建定时器对象 Timer t=new Timer(); //在3秒后执行MyTask类中的run方法 t.schedule(new MyTask(), 3000); } }
创建了一个Timer就相当于启动了一个新线程,这个新线程并不是守护线程,所以会一直运行.
在Time类和TimerTask类中都有一个cancel()方法.
TimerTask类中的作用是:将自身从任务队列中清除,(一个Timer对象可以执行多个Timertask任务)
Timer类中的作用是:将任务队列中的全部任务清空.
我给出的Date类型的时间,早于当前的时间.则会立即执行task任务.