一.进程
进程,是正在运行的程序实例,是操作系统进行资源分配的最小单位。每个进程都有它自己的地址空间和系统资源(比如CPU时间,内存空间,磁盘IO等)。多个进程可以同时执行,每个进程在运行时都不会影响其他进程的运行,资源不共享;
程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
二.线程
2.1线程的简介
2.1.1线程和进程的区别
线程是进程的一部分,是CPU能够进行运算调度的最小单位。线程不能独立存在,必须依赖于进程。线程是一个进程中的顺序执行流(执行单元)。一个进程中可以有一个线程,也可能有多个线程。每个线程都有自己的指令指针、堆栈和局部变量等,但他们共享进程的代码,数据和全局变量等资源
多线程可以实现并发执行,提高程序的效率。
线程和进程的区别:
1.进程是操作系统运行的一个任务,线程是进程中的一个任务
2.进程是资源分配的最小单元,线程是程序执行的最小单元
3.线程是轻量级的进程,一个进程中包含多个线程,多线程共享进程中的数据,使用相同的地址空间,因此,线程间的通信更加方便,CPU切换一个线程的开销比进程小很多
4.一个进程结束,其内部的所有线程都会结束,但是不会对另外的进程造成影响。多线程程序,一个线程结束,可能会造成其他的线程结束
2.1.2 CPU时间片
CPU时间片在单核处理器上时,一次只能运行某一个进程的某一个线程,如何公平处理,一种方法就是引入时间片的概念,让每个程序轮流执行。
CPU调度机制算法:会将时间划分成一个个的时间片,时间片的大小从几ms到几百ms;
线程调度:线程调度是计算机多线程操作系统中分配CPU时间给各个线程的过程。每个线程代表程序中的一个执行路径,操作系统通过线程调度器分配处理器时间,决定哪个线程将获得执行的机会,以及获得的时间长短。
进程调度:进程调度是操作系统中分配CPU时间给各个进程的过程。进程是系统进行资源分配和调度的独立单位,它包含代码、数据以及分配的系统资源。与线程调度不同,进程调度涉及到的上下文切换成本更高,因为进程间共享的资源更少。
串行和并发:
串行指同步运行,并行指异步运行
操作系统将时间划分成很多时间片段,尽可能的均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程处于等待状态。所以这种微观上是走走停停,断断续续的,宏观上都在运行的现象叫并发。
2.2线程的调度机制
2.2.1 Java线程的状态简介
在java中,线程可以处于一下几种状态;
新建状态(New):线程对象已经创建,但还没有调用start()方法。
就绪状态(Runnable):线程已经调用start()方法,等待CPU调度执行。
运行状态(Running):线程获得CPU时间片,开始执行run()方法里的代码。
阻塞状态(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,直到进入就绪状态。
等待状态(Waiting):线程因为某些条件而进入等待状态,此时不会被分配CPU时间片,直到其他线程显式地唤醒。
超时等待状态(Timed Waiting):线程在指定的时间内等待,时间到后会自动返回到就绪状态。
终止状态(Terminated):线程的run()方法执行完毕或者因异常退出而结束线程的生命周期。
2.2.2 抢占式调度与协同式调度
java线程的调度基本上是抢占式的,在这种模式下,每个java线程都有机会获得时间片,操作系统基于线程的优先级来决定哪个线程会优先运行。高级的线程会获得更多的运行机会。
而协同式调度需要线程主动释放控制权,当前线程必须要主动让出CPU时间,其他的线程才能有执行的机会,导致协同式调度在java中出现得较少。
2.2.3 线程的优先级
线程的切换是由线程调度来控制的,我们无权通过代码来干涉,但是可以通过设置线程的优先级来提高线程获取时间片段的概率。通过setPriority来设置优先级,优先级较高的线程有更大概率获得CPU时间片。
线程的优先级分为1-10,1最低,10最高,线程内部提供了3个关键字来表示最低,最高,默认优先级。
Thread.MIN_PRIORITY 表示最低优先级
Thread.MAX_PRIORITY 表示最高优先级
Thread.NORM_PRIORITY 表示默认优先级
2.2.4 线程生命周期管理
java虚拟机可以通过Thread类的方法来管理线程的生命周期,比如start()、sleep()、yield()、join()、wait()等,让线程在恰当的位置运行或者暂停。
start( )方法可以使线程处于就绪状态
yield( )方法可以使当前运行的线程让出自己的时间片,但不会阻塞线程,只是将线程从运行状态转移至就绪状态。
join( )方法可以让一个线程等待另一个线程完成后再继续执行。
sleep( )方法可以使当前线程暂停执行指定时间。
wait( )方法是当前线程释放锁,释放cpu等资源,进入等待状态
3.线程的创建及其常用的API
3.1 线程的三种创建方式
3.1.1第一种
使用Thread的实现类或者匿名内部类的方式创建一个线程;
1.实现Thread
class MyThread extends Thread{
//重写run方法: run方法就是用来编写线程的任务代码的
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
我们自定义一个类,继承Thread类,并重写里面的run方法, run方法就是用来编写线程的任务代码的,我们需要实现1-100的打印,所以设置一个for循环打印即可,在sout中,需要传入Thread.currentThread( ).getName( )来获取到当前线程的名字,后面加上 i 即可
public static void main(String[] args) {
//创建一个线程对象,处于新建状态
MyThread mt = new MyThread();
//启动线程,使其处于就绪状态
mt.start();
之后我们就可以在main方法中创建一个对象;然后通过对象来调用start方法,让线程启动。
2.使用匿名内部类的方式:
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<100; i++){
System.out.println("hello world");
}
}
};
//启动线程t2,使其处于就绪状态
t2.start();
System.out.println("-----main结束-----");
我们可以直接在main方法中创建一个匿名内部类对象来创建线程,直接在花括号中重写run方法,确定线程任务行为;打印100次hello world;之后同样需要启动线程。
3.1.2 第二种
使用Runnable的实现类或者匿名内部类的方式创建一个线程;
1.实现Runnable接口
class MyTask implements Runnable {
//重写run方法:线程的任务代码
public void run() {
int sum = 0;
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
sum += i;
}
}
System.out.println("sum: "+sum);
}
}
我们在自定义的类实现Runnable接口,重写里面的run方法,也就是任务所需的代码,我们希望打印出100以内的偶数和,
public static void main(String[] args) {
//获取Runnable的实例对象
Runnable task = new MyTask();
//创建线程对象,调用构造器Thread(Runnable runnable),传入任务
Thread t1 = new Thread(task);
//启动线程,进入就绪状态
t1.start();
之后在main方法中获取Runnable的实例化对象,然后创建一个线程对象,将Runnable的实例化对象传入,然后用线程对象启动线程就可以了;
2.使用匿名内部类的方式
Runnable r = () ->{
int sum = 0;
for(int i=0; i<100; i++){
if(i%2==1){
sum+=i;
}
}
System.out.println("sum="+sum);
};
Thread t2 = new Thread(r);
t2.start();
}
}
我们可以用Lambda的表达式()用于传递形参,{ }中写run方法的内容,我们希望打印100以内所有的奇数和,然后实例化一个线程对象,形参传入Runnable对象,并调用启动方法。
3.1.3 第三种
先获取一个Callable对象,重写里面的call方法,call相当于run,但是call方法有返回值,
之后获取一个FutureTask对象,将上面的Callable对象传入构造器,
之后获取Thread对象,将上面的FutureTask对象传入构造器;
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Callable 是函数式接口,里面的 V call () 相当于Thread或者Runnable的run方法,即任务代码书写的位置
Callable c1 = ()->{
int sum = 0;
for (int i = 2; i <=100 ; i+=2) {
sum+=i;
Thread.sleep(100);
}
return sum;
};
//调用FutureTask的构造器,传入Callable对象
FutureTask<Integer> ft = new FutureTask<>(c1);
//创建Thread线程对象,调用start方法进入就绪状态
Thread thread = new Thread(ft);
thread.start();
//获取线程结束之后的结果 注意:get方法有阻塞所在线程的效果
Integer result = ft.get();
System.out.println("result: "+result);
System.out.println("=====main方法结束======");
}
Callable的返回值返回后,我们把对象传入FutureTask构造器,创建出的对象传入Thread构造器;让对象调用启动方法,获取Callable返回的结果;并将结果打印出来。
3.2Thread的常用构造器
3.2.1 Thread(Runnable runnable)
我们在形参中传入一个Runnable的任务对象;
Runnable r = ()-> {
int a1 = (int)(Math.random()*100);
int a2 = (int)(Math.random()*100);
int a3 = (int)(Math.random()*100);
int a4 = (int)(Math.random()*100);
int a5 = (int)(Math.random()*100);
int[] arr = {a1,a2,a3,a4,a5};
//冒泡排序
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
};
new Thread(r).start();
System.out.println("==========单参结束==========");
首先可以通过Lambda表达式创建好一个任务对象,先创建一个有随机数的数组,然后对这个数组进行冒泡排序,然后打印该数组;之后可以调用new关键字创建线程对象,传入任务对象,之后调用start启动该线程。
3.2.2 Thread(Runnable target,String name)
传入的第一个形参是任务接口的对象,第二个形参是创建出的线程名字;
Thread abc = new Thread(r, "abc");
abc.start();
System.out.println("=========双参结束=========");
任务接口就传入我们刚才创建的任务r就可以,命名是线程对象为abc,然后通过线程对象abc来调用start方法来启动线程。
就会得到第二个冒泡排序的数组。
3.2.3 Thread(String name)
形参是创建线程对象的名字,
Thread xiaohua = new Thread("xiaohua");
3.3常用的属性和方法
//1.获取当前线程的对象
Thread current = Thread.currentThread();
//2.获取当前线程的名字
String name = current.getName();
//3.获取当前线程的唯一标识符
long id = current.getId();
//4.获取当前线程的优先级
int priority = current.getPriority();
//5.获取当前线程的状态
Thread.State state = current.getState();
//6.查看当前线程是否存活
boolean alive = current.isAlive();
//7.查看当前线程是否被打断
boolean interrupted = current.isInterrupted();
//8.查看当前线程是否为守护进程
boolean daemon = current.isDaemon();
System.out.println("Current thread name: " + name);
System.out.println("Current thread id: " + id);
System.out.println("Current thread priority: " + priority);
System.out.println("Current thread state: " + state.name());
System.out.println("Current thread alive: " + alive);
System.out.println("Current thread interrupted: " + interrupted);
System.out.println("Current thread daemon: " + daemon);
main方法的本质上就是一个线程;我们可以调用main线程来查看相关的属性和方法,可以获取当前线程的对象并获取当前线程的名字,与当前线程的标识符,优先级。调用getState来获取当前线程的状态,判断当前线程是否存货,是否被打断,以及查看当前线程是否为守护线程。
3.4守护线程的说明
一个线程要么是守护线程,要么是前台线程:
前台线程:在表面运行的,能看到的,或者daemon的值为false的;
后台线程:就是守护线程,daemon的值为true的。
注意:当所有的前台线程都结束了,后台线程即使有任务,也要立即结束。
案例演示:
//第一个线程:rose喊10次 i jump 10次后,真跳了
Thread rose = new Thread("rose"){
public void run(){
for(int i=1;i<=11;i++){
System.out.println(getName()+"说:I jump");
try{
Thread.sleep(500);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
System.out.println("------正在落水中------");
}
};
首先定义一个前台线程,rose在喊了10次 i jump 后,跳入大海,首先创建一个进程名字是rose,使用匿名内部类的方式创建进程,重写里面的run方法,循环打印10次 i jump,之后让程序休眠0.5秒再执行,需要使用try - catch 来捕获异常,在循环结束后,打印正在落水。
Thread jack = new Thread("jack"){
public void run(){
for(int i=1;i<=100;i++){
System.out.println(getName()+"说: you jump, I jump");
try{
Thread.sleep(500);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
};
之后设计第二个线程,名字是Jack,在循环打印100次 you jump , i jump 也让程序休眠0.5秒,此时我们应该判断哪个线程是守护线程,由于rose在喊完10次后会直接落水,此时jack就不应该继续喊了,所以jack应该是守护线程,当rose线程执行完后,jack应该立即停止执行。
//jack应该是守护进程,即rose跳水后,jack应该停止喊叫
jack.setDaemon(true);
rose.start();
jack.start();
3.5生命周期相关方法
3.5.1 sleep()方法
线程的睡眠方法 static void sleep(long time),可以传入一个参数时间,单位是毫秒,让线程里的当前代码休眠,进入阻塞状态;不再占用CPU的时间片,休眠时间一过,就会立即进入就绪状态,等待调度器分配时间片段
注意:在休眠期间可能会被打断,因此要处理异常 InterruptedException
还有一个重载方法 static void sleep(long time,int nanos),该方法的第二个参数是指单位是纳秒
public static void main(String[] args) {
Thread t1 = new Thread("download"){
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("正在下载视频中......"+(i*10)+"%");
//使用休眠方法来假装模拟正在下载中
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("========视频下载完成========");
}
};
//测试
t1.start();
}
我们首先定义了一个线程名字是download,并使用匿名内部类的方式重写run方法循环打印10次下载过程来模拟现实生活中下载视频的过程。在每次打印下载过程中让程序休眠一段时间,需要捕获异常;
3.5.2 yield( )方法
线程的礼让方法 static void yield(): 表示让出CPU的时间片段,进入就绪状态,不过,下一个时间片段还有可能是该线程的
Thread t1 = new Thread("Thread-A"){
public void run(){
for(int i=0;i<=9;i++){
System.out.println(getName()+":"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
首先我们定义了一个Thread-A,重写run方法,循环打印10次数字,并调用休眠方法每隔0.1秒打印一次,
Thread t2 = new Thread("Thread-B"){
public void run(){
for(int i=0;i<=9;i++){
//打印5之前,让一下时间片段
if(i==5){
//执行到28行,CPU正在被Thread-B使用
Thread.yield(); //这一行代码表示Thread-B让出CPU,进入就绪状态。下一个时间片段可能还会被线程调度器分配给Thread-B
}
System.out.println(getName()+":"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
之后我们创建线程b,同样是打印10次数字,但是当i = 5时,让出时间片,进入就绪状态,但是下一个时间片段可能还会被线程调度器分配给线程B,同样让程序每隔0.1秒打印一次。
3.5.3 join( )方法
另一个线程加入方法 void join():此时当前线程会进入阻塞状态,等待另一个线程结束之后 ,当前线程会进入到就绪状态;
案例演示:模拟图片的下载和显示过程,应该先下载图片,再显示图片
Thread download = new Thread("download"){
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println(getName() + "图片正在加载中... 进度条:" + i*10+"%");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("图片下载完成");
}
};
首先我们定义下载线程,循环打印10次进度条,并让程序休眠0.3s执行
Thread show = new Thread("show"){
public void run() {
try {
//当前线程是显示线程,显示应该该下载线程执行完毕之后,再执行,因此要让download加入进来
download.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 1; i < 11; i++) {
System.out.println(getName() + "图片正在显示中... 进度条:" + i*10+"%");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("图片显示完成...");
}
};
之后我们定义显示线程,重写任务代码中,我们需要考虑如果时间片段先分给了显示线程,需要让下载线程的代码都执行完毕才能继续执行显示线程,所以需要通过下载线程调用join方法,让下载线程加入,同样需要捕获异常,
3.5.4 interrupt()方法
线程中断,打断方法 void interrupt( );表示当前线程需要去打断另一个线程;
需要注意:是在当前线程中调用另一个线程的打断方法
public static void main(String[] args) {
Thread lin = new Thread("林永健"){
public void run() {
System.out.println(getName()+"说:开始睡觉了");
try {
//线程休眠100s,模拟林永健睡着了
Thread.sleep(100000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println(getName()+"说:干嘛呢?都破了相了");
}
}
};
首先我们定义林永健让他开始睡觉,并设置休眠时间,
Thread huang = new Thread("黄宏"){
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 0) {
Thread.yield();
}
System.out.println(getName() + "说:"+(i+1)+"个80" );
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(getName()+"说:搞定了");
//打断lin的睡觉
lin.interrupt();
}
};
之后我们定义黄宏的线程,通过循环来打印几个八十,在打印完最后一个80时,需要通过lin的线程调用interrupt方法叫醒lin,
4.临界资源问题
4.1什么是临界资源
在一个进程之中,多个线程之间资源是共享的,如果一个资源同时被多个线程访问,那么这个资源就是临界资源,当多个线程同时并发读写一个临界资源问题时,就会发生线程并发安全问题。
常见的临界资源:
多线程共享实例变量 多线程共享静态公共变量
如果想解决线程安全问题,就需要将异步的操作变为同步的操作,
异步操作:相当于各干各的,多个线程并发
同步操作:操作有先后的顺序,相当于你干完我再干。
4.2锁机制
4.2.1锁机制的简介
针对于临界资源安全隐患问题的解决方式,引入了锁机制
1.锁机制的作用:将异步的代码块变成同步的代码块
2.语法:
synchronized(锁对象的地址){
//需要同步的代码块(如果不同步,会出现安全隐患问题)
}
3.任何的java对象都可以作为锁,只有一个要求:所有的线程都是同一个对象即可。
4.同步的代码块尽量缩小范围,提高替他代码块的执行效率。
5.运行逻辑:
当一个线程A执行到{ },说明该线程已经获取到了锁对象,其他的线程都必须等待,直到线程A执行完了同步代码块,会自动释放锁对象,其他的线程才有机会获取到锁对象,谁获取到锁对象,谁就能执行同步代码块,没获取到锁对象的线程继续等待。
4.2.2案例演示:
class Desk1 implements Runnable{
//静态属性:豆子的数量,初始值10个
private static int beanCount = 10;
//拿走豆子,一次只能拿一个
public void take(){
beanCount--;
}
首先我们定义一个类Desk1,有私有的静态变量10个豆子,并提供一个拿豆子的方法,
public void run() {
while(beanCount!=0){ //桌子上只剩下一个豆子时,两个线程恰巧执行到条件判断。 1>0成立
//那么两个线程都进入循环体了,各自拿走一个豆子,此时豆子就是-1个了,明显不合理
//即出现了临界资源的安全隐患问题
try {
//增加出现安全隐患的概率
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//将具有安全隐患问题的代码放入{};因为两个线程操作的是同一个桌子,因此this可以作为锁对象
synchronized(this){
if(beanCount>0){
take();
}
}
System.out.println(Thread.currentThread().getName()+"拿走了一个豆子,剩下的个数是"+beanCount);
if(beanCount<=0){
break;
}
}
}
之后我们重写任务代码,当桌子上的豆子只剩下一个时,如果有两个线程同时去拿豆子,那么最后的豆子数量会变成-1,这明显不合理,就是临界资源安全隐患问题,
我们需要将会出现安全隐患的代码块编程同步的,即给这部分代码块上锁,使用synchronized关键字,()中传入对象,在这里我们使用this,如果豆子的数量大于0,拿走豆子,当拿到最后一个豆子时,会有一个线程获取到锁对象,那么他会把豆子取走,等到下一个线程获取到锁对象时,豆子的数量是0,那么就不会进入循环。
public static void main(String[] args) {
Desk1 desk1 = new Desk1();
Thread xiaoming = new Thread(desk1,"小明");
Thread xiaohong = new Thread(desk1,"小红");
//启动线程
xiaoming.start();
xiaohong.start();
}
}
在main方法中进行测试,首先创建一个Desk1对象,然后创建两个线程小明和小红,调用start方法进行测试。
class BlackBoard implements Runnable{
private int apple = 10;
public void run() {
while(true){
take();
System.out.println(Thread.currentThread().getName()+"拿走了一个苹果,剩下的个数"+apple);
if(apple == 0){
break;
}
}
}
/**
* 可以对非静态方法上锁,锁对象就是this
*/
public synchronized void take(){
if (apple >0){
apple--;
}
}
}
我们可以设计另一个类,在黑板上拿走苹果,实现Runnable接口,先定义实例变量10个苹果,然后再定义一个非静态方法take(),并对非静态方法上锁,在void前面加上synchronized即可,之后重写run方法,并通过循环拿走苹果,如果苹果的个数变为0就跳出循环。
public static void main(String[] args) {
BlackBoard bb = new BlackBoard();
Thread xiaoming = new Thread(bb,"小明");
Thread xiaohong = new Thread(bb,"小红");
xiaohong.start();
xiaoming.start();
}
在main方法中进行测试,新建两个线程小明和小红,并启动线程;
这样就可以有效的避免出现临界资源隐患问题。
4.3 synchronized 的作用域
上锁的范围是需要同步的代码块范围,如果要给非静态方法上锁,需要在方法前添加修饰词synchronized,当有一个线程访问了该方法时,就相当于获得了锁对象,其他线程如果像访问该方法,就去要处于等待状态,有一个前提,多个线程访问的实例对象必须是同一个,当某一个线程正在访问一个同步方法时,this这个锁对象即被他占用,其他线程想要执行该实例对象的其他方法也需要等待,因为该方法的所对象已经被占用了
class Desk implements Runnable {
//添加一个非静态属性,充当锁对象
Object lock = new Object();
private int BeanCount = 10;
//如果给方法中的所有代码上锁,不如直接在方法上添加synchronized关键字,给方法上锁
//此时不需要特意制定锁对象,因为锁对象是this
public synchronized void take() {
System.out.println("开始取豆子");
//给部分代码上锁
// synchronized (lock) { //lock作为锁
if (BeanCount > 0) {
BeanCount--;
}
// }
System.out.println("豆子被拿走了一个");
}
public void run() {
while(BeanCount > 0){
take();
}
}
public synchronized void sport() {
//...
}
}
在上述代码中,我们添加一个非静态属性,作为锁对象,并提供私有的属性10个豆子,如果需要给方法中的所有代码都上锁,不如直接给方法上添加关键字来上锁,此时不需要指定锁对象,因为锁对象是this,之后就可以重写run方法,
public static void main(String[] args) {
Desk desk = new Desk();
Thread t1 = new Thread(desk,"小明");
Thread t2 = new Thread(desk,"小红");
t1.start();
t2.start();
}
我们在main方法中测试,先创建一个desk对象,然后创建两个线程,并启动线程。
4.4单例模式的改进
懒汉式单例,在多线程的环境下,会出现问题。由于临界资源问题的存在,单例对象可能会被实例化多次。解决方案,就是将对象的 null值判断和实例化上锁,作为同步代码块执行。
class Boss{
//第一步:提供一个该类的私有的静态的该类的变量
private static Boss boss;
//第二步:将构造器私有化
private Boss(){}
//第三步:提供一个公有的静态的返回该类型的方法
//给静态方法上锁,就是给方法添加修饰词synchronized
//锁对象时类名.class,这个类对象在整个项目下都是唯一的
public synchronized static Boss getInstance(){
// synchronized (Boss.class){
if(boss == null){
boss = new Boss();
}
// }
return boss;
}
}
第一步先私有化静态变量,然后私有化构造器,提供返回值是该类型的公有方法,并对其上锁,如果对象的地址值是空,就新创建一个对象,返回该对象。这就是单例模式的懒汉模式。
4.5 死锁
死锁的产生原因:线程1先获取锁A,但是还想获取锁B,线程2获取了锁B,但还想获取锁A。两个线程都占用了对方想用的锁,而且对方还都占着锁不释放,因此都出现了等待现象,无法继续向下执行。
public static void main(String[] args) {
Thread t1=new Thread("小明"){
public void run(){
synchronized("A"){
for (int i = 0; i <50 ; i++) {
System.out.println(getName()+":"+i);
}
synchronized("B"){
for (int i = 50; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
}
};
Thread t2=new Thread("小红"){
public void run(){
synchronized("B"){
for (int i = 0; i <50 ; i++) {
System.out.println(getName()+":"+i);
}
synchronized("A"){
for (int i = 50; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
}
};
t1.start();
t2.start();
}
上述代码的执行结果只能是执行到49,无法执行到后迷案的,因为出现了死锁。
避免死锁的方式:按照顺序加锁,或者设置一个超时等候,如果在一定范围内没有获取到锁,那么就不执行锁中的代码块。
public static void main(String[] args) {
Thread t1=new Thread("小明"){
public void run(){
synchronized("A"){
for (int i = 0; i <50 ; i++) {
System.out.println(getName()+":"+i);
}
synchronized("B"){
for (int i = 50; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
}
};
Thread t2=new Thread("小红"){
public void run(){
synchronized("A"){
for (int i = 0; i <50 ; i++) {
System.out.println(getName()+":"+i);
}
synchronized("B"){
for (int i = 50; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
}
};
t1.start();
t2.start();
}
我们按照顺序给代码进行上锁,这样就不会出现死锁的情况了,因为都是同步代码,没有并行的代码。这样小明和小红都能执行到99.
4.6与锁相关的API
1.wait( ) 释放已经占有的锁对象,进入到等待队列中,不参与时间片的争抢,也不参与锁的争抢,需要等待其他线程调用通知方法。
有两个重载方法 wait(long timeout ): 指定一个等待的时间,超过这个时间会被自动唤醒
wait(long timeout , int nanos):指定等待的时间更为精确。
2.notify( ) 通知,唤醒等待队列的某一个线程,被唤醒的线程是随机的,开始参与锁对象的争抢
3.notifyAll( ) 通知,唤醒等待队列中的所有线程,都开始争抢锁对象。
我们可以通过上述的下载图片的案例来更好的理解这些方法:
public static void main(String[] args) {
//定义一个对象,充当锁
Object lock = new Object();
//定义一个下载线程
Thread down = new Thread(()->{
System.out.println("---开始下载图片---");
for (int i = 0; i < 11; i++) {
System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("---图片下载完成---");
synchronized (lock) {
//通知等待队列中的某一个线程
//如果想要调用锁的通知方法,那么也必须先获取锁对象
lock.notifyAll();
}
},"下载图片");
首先我们在main方法中定义了一个锁对象,之后开始定义下载线程,使用for循环,并设置一个休眠时间是200ms,在循环结束之后打印图片下载完成,之后通过上锁的方式通知所有线程开始抢锁
//定义一个显示线程
Thread show = new Thread(()->{
//该显示线程,纲要执行任务时,就应该进入等待队列,等待下载队列
try {
synchronized (lock) {
//如果想要进入等待队列,必须先获取锁对象,然后在调用锁对象的wait方法
lock.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//当下载线程结束后,该线程得到通知了,然后才有机会获取锁对象,然后获取时间片段,才能继续向下执行
System.out.println("---开始显示图片---");
for (int i = 0; i < 11; i++) {
System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("---图片显示完成---");
},"显示图片");
down.start();
show.start();
之后在显示线程中,我们需要先对线程进行休眠操作,让下载线程先执行,当下载结束之后,再执行显示线程。
4.7 ReentrantLock
4.7.1 可重入锁的简介
ReentrantLock指的是可以让一个线程多次获取锁的类型;里面内置了一个计数器用于记录当前线程获取了该锁的次数,可以有效地避免出现死锁的情况。
该类提供了两个子类型,非公平锁和公平锁,非公平锁执行的顺序不是按照线程的请求顺序,而是可能发生插队现象,这种锁执行效率更高,但是可能会增加线程的饥饿情况;
公平锁:多个线程获取锁的方式是按照线程的请求顺序,谁都能获取到,减少了线程的饥饿情况,但是会降低系统的吞吐量,在构造器种传入true则表示公平锁,传入false则表示非公平锁。
该类比使用synchronized关键字更加灵活,但是需要手动上锁或者解锁,
lock(): 上锁方法 锁对象没有被其他线程占用时,就会上锁成功,否则当前线程处于阻塞状态。unlock(): 解锁方法,必须在占有锁的时候,才能进行解锁,否则报异常;
tryLock(): 尝试获取锁,如果获取不到,并不阻塞,而是执行其他代码
获取不到锁返回false,获取到锁返回true
tryLock(long time , TimeUnit unit):
可以指定一定时间内获取锁对象,如果超过这个时间还没有获取到锁对象,就返回false,
指定时间内获取到锁,就返回true
class MyCounter implements Runnable {
private int count = 0;
private String name;
ReentrantLock lock = new ReentrantLock(true);
public MyCounter(String name) {
this.name = name;
}
public void run() {
//先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平
//调用lock方法,进行上锁
lock.lock();
for (int i = 0; i < 10; i++) {
count++;
System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//同步代码块执行完后,需要解锁,给别的线程获取锁的机会
lock.unlock();
}
我们首先定义一个MyCounter类,提供私有的成员变量;之后创建一个可重入锁对象lock,是一个公平锁,重写run任务方法,调用锁对象对需要同步的代码块进行上锁,代码执行完后,需要调用锁对象解锁。
public static void main(String[] args) {
//使用两个线程来模拟两个人来使用计数器
MyCounter counter = new MyCounter("秒表");
Thread t1 = new Thread(counter,"小明");
Thread t2 = new Thread(counter,"小红");
t1.start();
t2.start();
}
在main方法中,我们创建了一个该类的对象,并实例化两个线程,先启动小明,再启动小红。
4.7.2 可重入锁的标准写法
把释放锁的操作写在finally模块中
class MyCounter1 implements Runnable {
private int count = 0;
private String name;
ReentrantLock lock = new ReentrantLock(true);
public MyCounter1(String name) {
this.name = name;
}
public void run() {
//先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平
//调用lock方法,进行上锁
lock.lock();
for (int i = 0; i < 10; i++) {
count++;
System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 当在释放锁之前,发生了异常,那么锁就无法释放了,那么别的线程想要获取锁对象,变成了不可能,所以都会处于阻塞状态。
*
* 因此为了不阻塞其他线程对于锁的获取,当前线程不管是否有无异常,那都应该正确的释放锁
* 所以锁的释放应该放在try的finally模块里
*/
try {
String str = null;
System.out.println(str.length());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
//同步代码块执行完后,需要解锁,给别的线程获取锁的机会
再释放锁之前,如果代码片段发生了异常,那么此时锁就会无法释放,别的线程就不可能获取到锁对象,我们需要将解锁的方法放入到finally中实现.
public class TicketCenterDemo {
public static void main(String[] args) {
TicketCenter ticket = new TicketCenter(100);
Thread t1 = new Thread(()->{
try {
ticket.buyOne();
} catch (Exception e) {
throw new RuntimeException(e);
}
},"小明"
);
Thread t2 = new Thread(()->{
try {
ticket.buyOne();
} catch (Exception e) {
throw new RuntimeException(e);
}
},"小花");
Thread t3 = new Thread(()->{
try {
ticket.buyBatch(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
},"小强");
Thread t4 = new Thread(()->{
try {
ticket.buyBatch(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
},"小丽");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/**
* 购票中心
*/
class TicketCenter extends Thread{
public static ReentrantLock lock = new ReentrantLock();
int ticket ;
public TicketCenter(int ticket) {
this.ticket = ticket;
}
//购买一张高铁票
public void buyOne(){
boolean success = false;
try {
success = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(success){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);
try {
Thread.sleep(1000);
ticket--;
System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}else{
System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");
}
}
//黄牛买多张票
public void buyBatch(int num){
boolean success ;
try {
success = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(success){
if(ticket > 0&&num < ticket){
System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);
try {
Thread.sleep(10000);
ticket-=num;
System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}else{
System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");
}
}
}
上述代码是tryLock的案例演示,有助于增加理解.