目录
一、可重入锁
二、死锁
三、内存可见性问题
四、wait和notify
五、单例模式
六、生产者消费者模型
七、定时器
八、线程池
public class test {
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
synchronized(locker){
System.out.println("线程t的第一层锁");
synchronized(locker){
System.out.println("线程t的第二层锁");
}
}
});
t1.start();
}
}
这段代码大家认为结果是什么???“线程t的第二层锁”会被打印吗???
对于这种同一个线程加多次锁,线程不会处于阻塞状态,对应的操作是遇到“{”锁计数器+1,遇到“}”锁计数器-1,当计数器为0时锁释放。
1、死锁场景
(1)一个线程一把锁
对于上述的代码来说,如果锁不是可重入锁,则一个线程重复加锁两次就会出现死锁问题。
(2)两个线程两把锁
线程1获取锁a,线程2获取锁b,在以上锁都未释放时,线程1尝试获取锁b,线程2尝试获取锁a,两个线程会处于阻塞状态,出现死锁。
public class test1 {
public static void main(String[] args) {
Object a=new Object();
Object b=new Object();
Thread t1=new Thread(()->{
synchronized(a){
System.out.println("线程t1获取到a锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(b){
System.out.println("线程t2获取到b锁");
}
}
});
Thread t2=new Thread(()->{
synchronized(b){
System.out.println("线程t2获取到b锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(a){
System.out.println("线程t2获取到a锁");
}
}
});
t1.start();
t2.start();
}
}
看两个线程的状态为:BLOCKED,表明线程处于阻塞状态。
(3)N个线程M把锁
2、产生死锁的4个必要条件
(1)锁是互斥使用的,一个线程获取了锁A,另一个线程也想获取锁A就必须阻塞等待。
(2)锁是不可抢占的,一个线程获取了锁A,只有等线程主动释放了锁A,下一个线程才能获取锁A。
(3)请求保持,一个线程在拿到锁A后,在持有锁A的状态下尝试获取锁B。
(4)循环等待。
3、解决死锁
(1)增加锁的数量
(2)减少线程
(3)限制同一时间线程的运行个数
(4)规定加锁顺序
1、问题
import java.util.Scanner;
public class test2 {
private static int flag=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (flag==0){
}
System.out.println("线程t1结束");
});
t1.start();
Scanner sc=new Scanner(System.in);
flag=sc.nextInt();
}
}
上述代码若输入非0的数,代码就会打印“线程t1结束”并结束进程???
可以看到线程t1的状态仍为RUNNABLE,还未结束进程,为什么输入1未使线程t1结束???
以上就是内存可见性问题,对于线程t1中的指令flag==0,先从内存中取出flag的值并放入cpu的寄存器里,再拿出寄存器中的flag与0进行比较。由于上述循环执行的非常快,而指令中的flag值短时间内没有变化,访问内存的速度要比访问寄存器的速度慢许多,于是JVM就进行了优化,不再重复读取内存中flag的值,直接使用cpu寄存器中缓存的flag值,导致后续flag的值发生了改变但是循环不再使用内存中flag的值。
2、解决方法
在flag变量中加入volatile关键字,保证内存可见性,cpu必须每次访问内存中该变量。
import java.util.Scanner;
public class test2 {
private volatile static int flag=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (flag==0){
}
System.out.println("线程t1结束");
});
t1.start();
Scanner sc=new Scanner(System.in);
flag=sc.nextInt();
}
}
3、JMM(Java Memory Model )
上面的现象为:在上面的代码中,编译器发现,每次循环都要读取内存,开销太大,于是就把读取内存的操作优化为读取寄存器操作,提高代码运行效率。
JMM模型表述:在上面代码中,编译器发现,每次循环都要读取“主内存”,开销太大,于是就把“主内存”的值复制到“工作内存”(寄存器+缓存)中,后续循环直接读取“工作内存”的值。
1、wait作用
当某个线程获取到锁后,发现当前还不满足执行的条件,就可以调用对象锁的wait方法,主动放弃被调度的机会,进入等待状态。
2、notify作用
第一个线程不满足条件后通过wait方法进入阻塞状态,其他线程在运行时使条件满足,此时就可以调用notify方法唤醒第一个线程。
3、wait和notify使用例子
有三个线程:t1,t2,t3,每个线程打印自己的名称,同时启动,并按t3,t2,t1的顺序打印
从题目获取到信息:先运行线程t3,当t3打印完时再运行t2,当t2打印完时再运行t1。即:线程t2与线程t1刚开始需要wait,当线程t3打印完后notify线程t2,当线程t2打印完后notify线程t1。
需注意:wait是要释放锁,释放的前提是要有锁,即:wait方法需在锁中调用,锁对象.wait();notify方法也需在锁中执行(java规定),需要唤醒哪个线程,就使用该线程的锁对象,锁对象.notify()
public class test3 {
public static void main(String[] args) {
Object lock1=new Object();
Object lock2=new Object();
Thread t1=new Thread(()->{
synchronized(lock1){
try {
lock1.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("线程t1");
});
Thread t2=new Thread(()->{
synchronized(lock2){
try {
lock2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程t2");
synchronized (lock1){
lock1.notify();
}
});
Thread t3=new Thread(()->{
System.out.println("线程t3");
synchronized (lock2){
lock2.notify();
}
});
t1.start();
t2.start();
t3.start();
}
}
4、wait和notify运行机制
当线程调用wait方法后,内部干了三件事:释放锁;线程进入阻塞状态;等待其他线程调用notify,其他线程调用notify后,线程重新拿到锁(可能也需要和其他线程竞争)。正常情况下:先执行wait,在执行notify
5、wait和notify的方法
不带时间的wait(),若没有其他线程调用notify()就死等;
带时间的wait(),若没有其他线程调用notify()且时间超过给定时间,线程就不再等待,继续运行。
notify()方法:若有多个线程被阻塞等待,且锁对象相同,调用notify()后就只能随机唤醒其中一个;
notifyAll()方法:若有多个线程被阻塞等待,且锁对象相同,调用notifyAll()后,多个线程被同时唤醒,此时会存在锁竞争。
6、wait()、join()、sleep()区别
wait():是object类的方法;没有时间的wait就只能通过其他线程调用notify方法被唤醒;有时间的wait在达到给定时间后未被唤醒,线程就不再阻塞;wait()必须在锁中使用。
join():是Thread类的方法;没有时间的join就只能在被等待的线程运行完成后才会被唤醒;有时间的join在达到给定时间后未被唤醒,线程就不再阻塞
sleep():是Thread类的方法;sleep()操作不需要锁,在达到给定时间后就会被唤醒,若想提前唤醒就只能通过interrupt唤醒(异常唤醒)。
1、概念
单例模式是一种设计模式,单例指某个类,在一个进程中,只允许创建一个实例。
2、饿汉模式
package single;
class singleDon{
private static singleDon example=new singleDon();
private singleDon(){
}
public static singleDon getExample() {
return example;
}
}
public class test {
public static void main(String[] args) {
singleDon example=singleDon.getExample();
}
}
每个类的类对象只会存在一个,即:在上述代码中只会存在一个example实例。且该实例在类加载是就已创建好。构造方法为private,使得该类无法创造出其他实例,只能调用getExample()方法获取实例。
3、懒汉模式
package single;
class singleTonLazy{
private static singleTonLazy example;
public static singleTonLazy getExample(){
if (example==null){
example=new singleTonLazy();
}
return example;
}
private singleTonLazy(){
}
}
public class test1 {
public static void main(String[] args) {
singleTonLazy example1 =singleTonLazy.getExample();
}
}
与饿汉模式的不同点在于:懒汉模式的实例并没有随着类的加载而创建好,而是需要是再调用getExample()方法创建实例(实例为空时)。
4、有关单例模式的线程不安全问题
上述两种模式有无线程不安全问题???
饿汉模式没有线程不安全问题。饿汉模式相当于只进行了读,读不会引起线程安全问题,每次读到的都是实例。
懒汉模式有线程不安全问题。试想一下:线程A运行到if后的判空语句,还未进行创建实例,此时cpu开始调度线程B,由于线程A未创建对象,线程B判断为空创建了对象,线程B执行完成后接着执行A又创建了对象,导致该类创建2个实例,不是单例模式。
5、懒汉模式改进
对于上述线程不安全问题,可以将if和new两个操作打包成一个原子(加锁),问题来了锁加在哪里合适呢???
若直接加在if上,那么对于后面已经创建了对象的,就只是一个读操作,读操作没有线程不安全问题,若还有锁运行效率是否有点低???所以应该进行if判断,若有对象直接返回,没有对象时再将if和new打包成一个原子创建对象。
package single;
class singleTonLazy1{
private static singleTonLazy1 example;
static Object lock = new Object();
public static singleTonLazy1 getExample(){
if(example==null){ //第一个if判断是否要加锁
synchronized(lock){
if (example==null){ //第二个if判断是否要创建对象
example=new singleTonLazy1();
}
}
}
return example;
}
private singleTonLazy1(){
}
}
public class test2 {
public static void main(String[] args) {
singleTonLazy example1 =singleTonLazy.getExample();
}
}
6、指令重排序
对于上述的代码还存在指令重排序问题。
example = new singleTonLazy1() 这一指令中有三个步骤:①申请一段内存空间;②在内存上调用构造方法创造出实例;③将内存地址赋值给example1变量。这一指令可以按照①②③运行,也可以按照①③②运行,对于单线程来说,指令重排序不会造成什么影响,最后都是实例化example1对象,但若对多线程就不一定了。假如线程A对于这一指令按照①③②的步骤运行,运行到③步骤时,example1不为空,但尚未初始化,此时又开始调度线程B,example1不为空,但若要使用example1的属性时,此时还未初始化就会出现问题,这就是指令重排序问题。
如何解决指令重排序问题???在上面谈论内存可见性问题时我们提到:给变量加入volatile关键字,可以保证内存可见性,cpu必须每次访问内存中该变量。同样,我们若给变量加入volatile关键字,也可以杜绝指令重排序,可以按照原本的①②③步骤执行,就不会出现变量未初始化问题。
1、阻塞队列
阻塞队列是基于普通队列做出的扩展,相对于普通队列来说:①阻塞队列是线程安全的;②阻塞队列具有阻塞功能。对一个已经满了的队列进行入队列时,入队列操作会被阻塞,一直阻塞到有元素出队列(队列不满时);对一个已经空了的队列进行出队列时,出队列操作会被阻塞,一直阻塞到有元素入队列(队列不空时)。
2、基于阻塞队列下的生产者消费者模型
(1)解耦合
上图为“分布式分布系统”,服务器整个的功能不是由一个服务器全部完成的,而是每个服务器负责一部分功能,通过服务器之间的网络通信,最终完成整个功能。如:客户端想获取到主页信息,则将该请求发给A入口服务器,A将请求分别发给用户服务器和商品服务器,获取到用户信息和商品信息后,再将它通过A返回到客户端,形成完整的主页信息。该模型中,A和B以及A和C之间耦合性比较强,如果B或者C挂了,则A服务器也可能挂了。
上图为生产者消费者模型,引入了阻塞队列(消息队列),A服务器将请求发送给阻塞队列,用户服务器和商品服务器直接去阻塞队列中获取请求,降低了A和B以及A和C之间的耦合性。
(2)削峰填谷
在上述模型中,若同一时间,客户端的请求突然变多,此时很可能会导致服务器B和服务器C挂了
上图为生产者消费者模型,引入了阻塞队列(消息队列),A服务器将请求发送给阻塞队列,用户服务器和商品服务器直接去阻塞队列中获取请求,B服务器和C服务器获取消息的频率是稳定的,即使客户端请求变多,B和C也只是从阻塞队列中稳定的获取请求。
3、Java中有关阻塞队列的标准库
BlockingQueue是一个接口,该接口下有:链表阻塞队列、数组阻塞队列、优先级阻塞队列。
阻塞队列拥有的方法,put是带有阻塞功能的入队列,take是带有阻塞功能的出队列。
4、阻塞队列底层实现
基于数组实现阻塞队列(环形队列)
class myArrayListQueue{
private String[] elems=null;
private int head=0;
private int tail=0;
private int size=0;
public myArrayListQueue(int n){
elems=new String[n];
}
Object lock=new Object();
public void put(String elem) throws InterruptedException {
synchronized (lock){
if(size>=elems.length){
lock.wait(); //满了被阻塞
}
elems[tail]=elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
lock.notify(); //唤醒take阻塞
}
}
public String take() throws InterruptedException {
String elem=null;
synchronized (lock){
if (size==0){
lock.wait(); //空了被阻塞
}
elem=elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
lock.notify(); //唤醒put被阻塞
}
return elem;
}
}
以上代码写的对不对???试想一下:put中的notify就只能唤醒被阻塞的take吗???take中的notify就只能唤醒被阻塞的put吗???假如此时有三个线程:A、B、C,线程A因为空被阻塞,线程B因为空被阻塞,线程C进入take方法,放入元素后唤醒了A(也可能是B),此时A出元素,并唤醒线程,而被唤醒的线程也可能是B(锁对象一样),但此时队列是空的,出元素异常。所以我们希望在B被唤醒后在进行判断,如列表为空依旧被阻塞,列表不为空则放入元素。考虑while
class myArrayListQueue{
private String[] elems=null;
private int head=0;
private int tail=0;
private int size=0;
public myArrayListQueue(int capacity){
elems=new String[capacity];
}
private Object lock=new Object();
public void put(String elem) throws InterruptedException {
synchronized (lock){
while(size>=elems.length){
lock.wait(); //满了被阻塞
}
elems[tail]=elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
lock.notify(); //唤醒take阻塞
}
}
public String take() throws InterruptedException {
String elem1=null;
synchronized (lock){
while(size==0){
lock.wait(); //空了被阻塞
}
elem1=elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
lock.notify(); //唤醒put被阻塞
}
return elem1;
}
}
public class test {
public static void main(String[] args) {
myArrayListQueue queue=new myArrayListQueue(1000);
Thread t=new Thread(()->{ //生产者
int n=1;
while (true){
try {
queue.put(n+" ");
System.out.println("生产元素:"+n);
n++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t1=new Thread(()->{ //消费者
while (true){
try {
String n=queue.take();
System.out.println("消费者元素:"+n);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t1.start();
}
}
1、概念
设置一个等待时间,当时间到了之后就执行某个线程。
2、java中提供的定时器实现
import java.util.Timer;
import java.util.TimerTask;
public class test1 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("线程 3000");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("线程 2000");
}
},2000);
System.out.println("主线程");
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("线程 1000");
}
},1000);
}
}
Timer是实现定时器的工具,通过schedule方法向定时器中加入定时任务(TimerTask)和定时时间(delay),这个时间是相对时间间隔,以当前时间为基准,计算delay时间后运行。定时器内部有一个线程,在任务时间到了之后线程就会运行相应代码。在所有任务运行完成后,进程也没有结束,因为Timer中的线程为前台线程,运行完后就处于“严阵以待”状态,等待下一个任务安排。若想结束前台线程,可以调用cancel方法结束。
3、定时器底层实现
结合上述,我们需要一个线程t去运行任务;需要一个优先级队列,按等待的时间放入队列。需要一个schedule方法,向队列中放入任务并唤醒因为空等待的线程;线程t扫描队列运行任务,队列为空进入等待;任务等待时间还未到,进入等待,但不能用sleep等待,因为该过程需要释放锁以防队列加入等待时间更短的任务,任务等待时间已到执行任务,并从队列中删除任务。
import java.util.PriorityQueue;
//定义任务
class myTimerTask implements Comparable{
private long time;
private Thread thread;
public myTimerTask(Thread thread,long delay){
this.thread=thread;
this.time=System.currentTimeMillis()+delay;
}
@Override
public int compareTo(myTimerTask o) {
return (int)(this.time-o.time);
}
public long getTime() {
return time;
}
public void run(){
this.thread.run();
}
}
//定义线程t和队列
class myTimer{
PriorityQueue queue=new PriorityQueue<>();
Thread t;
Object lock=new Object();
public void schedule(Thread thread,long delay){
myTimerTask task=new myTimerTask(thread,delay);
synchronized(lock){
queue.offer(task);
lock.notify();
}
}
public myTimer(){
t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
synchronized (lock){
if(queue.isEmpty()){
try {
lock.wait();
} catch (InterruptedException e) {
break;
}
}
myTimerTask task1=queue.peek();
long curtime=System.currentTimeMillis();
if(curtime>=task1.getTime()){
task1.run();
queue.poll();
}else {
try {
lock.wait(task1.getTime()-curtime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
t.start();
}
public void cancel() throws InterruptedException {
Thread.sleep(8000);
System.out.println("定时器结束工作");
t.interrupt();
}
}
public class test {
public static void main(String[] args) throws InterruptedException {
myTimer timer=new myTimer();
timer.schedule(new Thread() {
@Override
public void run() {
System.out.println("线程3000");
}
},3000);
timer.schedule(new Thread(){
@Override
public void run() {
System.out.println("线程2000");
}
},2000);
timer.schedule(new Thread() {
@Override
public void run() {
System.out.println("线程1000");
}
},1000);
System.out.println("主线程");
timer.cancel();
}
}
1、概念
把要使用的线程提前创建好,用完了也不直接释放,而是放入池中,下次使用的时候直接在池中拿即可。这样做节省了线程创建/释放的资源开销。
2、为什么从线程池里取线程比系统申请更高效???
因为从线程池取线程是纯粹的用户态(可控)操作,从系统创建线程涉及到内核态(不可控)和用户态的切换
3、java标准库中的线程池
java的线程池和ThreadPoolExector类有关,该类最多可以有7个参数。
(1)int corePoolSize:核心线程数,指在一个线程池里最少得有多少个线程;
(2)int maximumPoolSize:最大线程数,指在一个线程池里最多能有多少个线程;
(3)long keepAliveTime:除核心线程以外,允许其他线程空闲时间,若其他线程超过时间阈值,则该线程就会被销毁;
(4)TimeUnit unit:时间单位(ms,s,min,hour....);
(5)BlockingQueue
(6)ThreadFactory threadFactory:线程工厂类,创建线程对象。
工厂模式:解决构造方法的不足。
例如:用point类表示坐标上的一个点:
class point{
public x;
public y;
public point(int x,int y){ //直角坐标系下
this.x=x;
this.y=y;
}
public point(int r,int a){ //极坐标系下
this.x=r.cosa;
this.y=r.sina;
}
}
以上代码编译能通过吗???不能通过,因为构造方法之间没有构成重载。
为了解决上述问题,我们引入了工厂模式,使用普通的方法创建对象。
class Point{
public static Point makePoint(int x,int y){
Point p=new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePoint1(int r,int a){
Point p=new Point();
p.setX(r*cosa);
p.setY(r*sina);
return p;
}
}
public class test{
public static void main(String[] args){
Point p=Point.makePoint(x,y);
Point p1=Point.makePoint1(r,a);
}
}
通过静态方法封装new操作,在方法内部设置不同的属性完成对象初始化,构造对象的过程就是工厂模式。
(7)RejectedExecutionHandler handler:拒绝策略,即当任务队列已经满了,此时若继续往队列中添加任务,线程池会怎么做???
①第一种策略:抛出异常,结束运行;
②第二种策略:新添加的任务由添加任务的线程执行,不由线程池中的线程执行;
③第三种策略:线程放弃执行最早的任务,执行最新的任务;
④第四种策略:新添加的任务不执行,线程池中的线程不执行新任务,添加任务的线程也不执行。
4、Executors 工厂类
ThreadPoolExector类用起来太复杂,因此标准库还提供了另一个和线程池有关的类:Executors。
工厂类,通过这个类来创建不同的线程池对象。
(1)newSingleThreadExecutor() :指线程池里只有单个的线程;
(2)newScheduledThreadPool(int corePoolSize):类似于定时器,可以延时执行任务;
(3)newCachedThreadPool():线程数目可以动态扩容;
(4)newFixedThreadPool(int nThreads):线程数目是固定的;
通过以上方法创建一个线程池对象(ExecutorService),并通过submit方法向线程池中添加任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test1 {
public static void main(String[] args) {
ExecutorService service= Executors.newFixedThreadPool(4);
service.submit(new Thread(){
@Override
public void run(){
System.out.println("线程1");
}
});
System.out.println("主线程");
}
}
5、创建线程池的时候,很多时候都需要设定线程的个数,那么这个数目设定为多少合适???
对于这个问题必须具体问题具体分析,要关注一个线程是cpu密集型任务,还是IO密集型任务。例如:一个进程中,如果所有的线程都是cpu密集型任务,每个线程都是在cpu上运行的,此时线程数目就不应该超过N(cpu逻辑核心数);若一个进程中,如果所有的线程都是IO密集型任务,cpu消耗非常少,此时线程数目就可以远远超过N。上述两种是极端情况,对于实际情况来说,cpu密集型任务和IO密集型任务线程数目的比例不清楚,可以通过不断实验确定最合适的线程数目。
6、底层实现线程池
(1)需要有一个阻塞队列,该队列中装要执行的任务;
(2)需要有一个构造方法,参数为线程池中线程的个数,创建线程,并不断扫描队列执行任务;
(3)需要有一个submit方法,往队列中放任务
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class myExectorPool{
BlockingQueue queue=new ArrayBlockingQueue<>(1000);
public myExectorPool(int n){
for (int i=0;i{
while(true){
try {
Thread t1=queue.take();
t1.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public void submit(Thread t) throws InterruptedException {
queue.put(t);
}
}
public class test2 {
public static void main(String[] args) throws InterruptedException {
myExectorPool myexectorpool=new myExectorPool(4);
for (int i=0;i<1000;i++){
int n=i;
myexectorpool.submit(new Thread(){
@Override
public void run() {
System.out.println("执行任务"+n+" 执行的线程名"+Thread.currentThread().getName());
}
});
}
}
}