一、多线程基本认识
1、程序、进程、线程
程序(program):指代完成指定任务并使用某种语言编写的一组指令的集合,也指代一段静态的代码。
进程(process):程序的一次执行过程,也可以用一个正在运行的程序来表示进程。它有自己的一个生命周期,自身产生、存在与消亡的过程。
线程(thread):进程中可以细化为线程,我们平时使用的就是主线程,我们也可以开辟其他线程来帮我们并行做其他事。若一个进程同一时间并行执行多个线程,就是支持多线程的!
线程作为调度和执行的单位,每个线程都有自己独立的运行栈和程序计数器(pc),并且线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,从同一堆中分配对象,可以访问相同的变量和对象,但多个线程共享的系统资源可能就会带来安全的隐患。
单线程与多线程见图:
2、认识单核与多核CPU
单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。好比高度公路有多个车道但是只有一个工作人员收费。由于CPU时间单元短,我们平时运行程序时也不会感觉出来。
多核CPU:能够更好的发挥多线程的效率,现在我们一般电脑都是多核的,服务器也是。
例如我的电脑是8核的,对于暂时学习做一些普通的项目都是够得:此电脑-右击管理—设备管理器即可查看
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vyr2szlY-1612970769700)(C:\Users\93997\AppData\Roaming\Typora\typora-user-images\image-20210204215155208.png)]
补充知识点:一个Java应用程序例如java.exe,其实最少有三个线程如main()主线程,gc()垃圾回收线程,异常处理线程。一旦发生异常的话就会影响主线程。
并行:多个CPU同时执行多个任务,例如多个人同时做不同的事情。
并发:一个CPU(采用时间片)同时执行多个任务。比如秒杀项目,多个人做同一件事。
3、多线程优点
背景介绍:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用时更短,但为什么仍需要使用多线程呢?
单核时,采用多线程会比采用单线程会更慢,因为进行多线程的过程中需要来回不断切换线程。
多核时,采用多线程就会比采用单线程快了,此时不需要来回进行切换。
多线程优点:
提高应用程序的响应,尤其是在图形化界面中更会使用到,增强用户的体验。
提高计算机系统CPU的利用率。
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,方便于理解与修改。
何时需要使用多线程:
程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,例如用户输入、文件读写操作、网络操作、搜索等。
需要后台运行的程序。
4、一个以上的执行空间说明
《head first java 2.0》中的一个问题:有一个以上的执行空间代表什么?
当我们创建多个线程,有超过一个以上的执行空间时,看起来会像是有好几件事情同时发生。实际上,只有真正的多处理器才能够同时执行多件事情(前面也提到了)。
对于使用在Java中的线程可以让它看起来好像同时都在执行中,实际上执行动作在执行空间中非常快速的来回交换,所以我们会有错觉每项任务都在同时进行,这里说个数字在100个毫秒内目前执行程序代码会切换到不同空间上的不同方法。
这里又有一个问题:Java在操作系统中也只是个在底层操作系统上执行的进程。一旦轮到Java执行时,Java虚拟机会执行什么?
目前执行空间最上面的会执行。
书中的截图:描述线程执行空间
二、线程的创建与使用
认识Thread类
Java的JVM允许程序运行多个线程,jdk也提供了相应的API,Thread类
Thread类:
我们想让一个线程做指定的事情是要通过某个特定Thread对象的run()方法来完成操作的,将run()方法的主体成为线程体。
想让run()中的内容执行并不是直接调用run()方法,而是调用其Start()方法。
为啥不调用run()方法呢?因为run()方法是我们进行重写的,若是直接调用run()方法来启动会当做普通方法调用的,那么就是主线程来执行了;调用start()方法启动线程,整个线程处于就绪状态,等待虚拟机调度,执行run方法,一旦run方法结束,此线程终止。
Thread类的构造器:
Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
常用方法:
void start():启动线程,并执行对象的run()方法。
String getName():返回线程的名称。
void setName():设置该线程名称。
static native Thread currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤。
static native void yield():表示放弃的意思,表示当前线程愿意让出对当前处理器的占用,需要注意就算当前线程调用其方法,程序在调度时,也还是可能会执行该线程的。
static native void sleep(long millis):指定millis毫秒数让当前线程进行睡眠,睡眠的状态是阻塞状态。
final void join():在线程a中可以调用线程b的join()方法,此时线程a进入阻塞状态,知道线程b执行完之后,a才结束阻塞状态。内部调用的是Object的wait()方法。
两种创建线程方式
1、创建线程方式一:继承Thread
为什么要继承Thread类呢?我们想要让其他线程执行自己的事情那么就需要先继承Thread类并重写run()方法,这样我们创建自定义线程类实例,调用start()方法即可。
这个run()方法其实是Thread类实现Runnable接口里的方法。
继承Thread方式:
class MyThread extends Thread{ @Override public void run() { for (int i = 0;i<100;i++){ System.out.println(i); } } } public class Main { //本身main方法是主线程 public static void main(String[] args) { MyThread thread = new MyThread(); thread.start();//①虚拟机中创建一个线程 ②调用线程的run()方法 //主线程下执行语句 for(int i=0;i<50;i++) System.out.println("主线程中循环:"+i); } }
可以看到主线程与自己创建的线程进行交互执行任务
注意点:
在程序中两个线程在不断的切换执行,他们的打印顺序不会一样,run()方法由JVM调用,什么时候调用执行过程控制都有操作系统的CPU调度(CPU调度策略,主要执行进程的顺序)决定。
记住要使用run()方法需要调用start()方法,若是调用run(),那么就只是普通方法,并没有启动多线程。
一个线程对象只能调用一次start()方法启动,如果重复调用了,会抛异常IllegalThreadStateException。当调用了start()后,虚拟机jvm会为我们创建一个线程,然后等这个线程第一次得到时间片再调用run()方法。
还可以使用Thread的匿名实现类来创建线程:
//本身main方法是主线程 public static void main(String[] args) { //使用Java8的函数式编程 new Thread(()->{ System.out.println("Thread的匿名实现类"); }).start(); //普通重写方法 new Thread(){ @Override public void run() { System.out.println("匿名实现类"); } }.start(); }
2、创建线程方式二:实现Runnable接口
通过实现Runnable接口方式来创建对象,实现该接口的类作为参数传递到构造器中:
class MyRunnable implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); } } public class Main { public static void main(String[] args) throws InterruptedException { //方式一:通过实现接口类,并创建实例作为参数放置到Thread类中 Thread thread = new Thread(new MyRunnable()); thread.start(); //方式二:通过给Thread类传入匿名接口类 new Thread(new MyRunnable(){ @Override public void run() { System.out.println("匿名接口类runnable"); } }).start(); } }
这里演示了两种使用实现Runnable接口方式来创建线程
源码解析:对于非继承方式实现Runnable接口在进行start()时进行调用作为参数中的run()方法
//构造器传入接口实现类,采用多态方式,调用了init()方法将target传入 public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } //init()方法 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { .... } //当调用start()方法实际上也会调用run()方法,看一下实际上就是调用的target.run()方法也就是之前传入的target @Override public void run() { if (target != null) { target.run(); } }
对于实现Runnable接口来创建多个线程方式:
class MyRunnable implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); } } public class Main { public static void main(String[] args) throws InterruptedException { MyRunnable runnable = new MyRunnable(); //若是要多线程调用同一个接口实现类的run()方法,就需要创建多个Thread实例 Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } }
比较两种创建方式
对于线程中共享的属性声明介绍:
继承Thread类实现的,想要共享其中的属性,那么需要使用static来进行修饰
实现runnable接口的,只要是new Thread(runnable)中的runnable实现类是同一个时,其中属性默认就是共享的。
那么哪个方式我们更常使用呢?
开发中优先选择实现runnable接口的方式
实现方式没有类的单继承局限性。
实现方式更适合来处理多个线程有共享数据的情况。
降低了线程对象和线程任务的耦合性。
常用方法
修改线程名
//方式一:使用setName(threadname)方法更改,需要创建实例之后 new Thread(){ ...run() }.setName("线程一"); //方式二:创建实例时,使用有参构造器修改线程名 Thread(String name) new Thread("线程一") //方式三:双参构造(实现runnable接口情况下) new Thread(myRunnable, "线程1");
修改了线程名,我们总要输出线程名吧:
class MyThread extends Thread{ @Override public void run() { //方式一:在线程中直接调用getName() System.out.println(this.getName()); } } public class Main { public static void main(String[] args) { new MyThread().start(); //方式二:通过Thread的静态方法currentThread()获取当前线程实例,调用方法即可 System.out.println(Thread.currentThread().getName()); } }
yield()方法 class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 20; i++) { if(i%4==0){ //使用该方法会交出当前cpu的占用,让其他线程执行 yield(); } System.out.println(Thread.currentThread().getName()+":"+i); } } } public class Main { public static void main(String[] args) { new MyThread().start(); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }
这里测试的是当i%4==0时让出占用的cpu资源,那么就会执行其他线程了
注意:使用yield()方法并不是每一次都有效的,也有可能继续执行当前线程,理解含义即可。
join()方法 class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } } public class Main { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); for (int i = 0; i < 10; i++) { if(i == 5){ try{ //此时主线程阻塞,开始执行b线程,b线程执行完以后,主线程阻塞结束 mythread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+":"+i); } } }
当i==5时,我们让主线程阻塞,来执行指定线程中内容,当指定线程执行完,主线程阻塞结束继续执行
sleep()方法 class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { if(i%2 == 1){ try { //让该线程睡眠(阻塞)1秒 sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+":"+i); } } } public class Main { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }
这里是当i%2==0时该线程进入睡眠(阻塞)1秒。
注意点:sleep()是Thread类中静态方法,在哪个线程中调用该方法哪个线程就会进入睡眠,就算你在a线程中调用b线程的sleep()也还是a线程进入睡眠。
线程优先级设置
介绍调度
CPU通过为每个线程分配CPU时间⽚来实现多线程机制,对于操作系统分配时间片给每个线程的涉及到线程的调度策略。
抢占式:高优先级的线程抢占CPU
针对于Java的调度方法:
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对于高优先级,使用优先调度的抢占式策略
线程优先级
Java中线程的优先级等级如下,定义在Thread类中的静态属性:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5
设置与获取当前优先级方法:
setPriority(int newPriority) :改变线程的优先级 getPriority() :返回线程优先值 //优先值可以直接使用Thread类中的静态变量 myThread.setProority(Thread.MAX_PRIORITY);
对于优先级的说明:
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才调用。
线程的分类
Java中的线程分为两类:守护线程与用户线程
用户线程:我们平常创建的普通线程。
守护线程:用来服务于用户线程,不需要上层逻辑介入。当线程只剩下守护线程时,JVM就会退出,若还有其他用户线程在,JVM就不会退出。
这两种线程几乎是一样的,唯一的区别是判断JVM何时离开!
如何设置线程为守护线程呢?
在调用start()方法前,调用Thread.setDaemon(true)。就可以把一个用户线程变成一个守护线程。
java垃圾回收就是一个典型的守护线程,若是jvm中都是守护线程时,JVM就会退出。
守护线程应用场景:
在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用,免去了有些时候主线程结束子线程还在执行的情况,当jvm只剩下守护进程时,JVM就会退出。
相对于用户线程,通常都是些关键的事务,这些操作不能中断所以就不能使用守护线程。
三、线程的生命周期
Thread.State中的六种状态
线程也具有生命周期,在JDK中使用Thread.State类来定义线程的几种状态,下图是六种状态:
NEW(初始):新创建的一个线程对象,还没有调用start()。
RUNNABLE(运行):Java线程中将就绪(ready)和运行中(running)两种状态笼统称为"运行"。若调用了satrt()方法,该状态线程位于可运行线程池中,等待被线程调度选中,获取CPU使用权限,此时处于就绪状态(ready)。就绪状态线程获得CPU时间片后变为运行中状态(running)。
BLOCKED(阻塞):线程处于阻塞状态,等待监视锁。
WAITING(等待):在该状态下的线程需要等待其他线程做出一些特定动作(通知或中断)。
TIMED_WAITING(超时等待):调用sleep()、join()、wait()方法可能导致线程处于等待状态,不同于WAITING,它可以在指定时间后自行返回。
TERMINATED(终止):表示线程执行完毕。
参考文章:Java线程的6种状态及切换(透彻讲解)
详细说明:
初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
生命周期中五种状态
Java语言使用Thread类及其实现类的对象来创建使用线程,完整的生命周期中通常要经历如下的五种状态:
新建—就绪—运行—阻塞—死亡
新建:当一个Thread类或其子类的对象被声明并被创建时,新生的线程对象处于新建状态。
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。
死亡:线程完成了它的全部工作(run方法结束)或线程被提前强制性地中止或出现异常导致结束。
周期图如下:
四、线程的同步
1、多窗口卖票(引出问题)
继承Thread与实现Runnable接口两种方式
多窗口卖票问题描述:我们在run()方法中模拟卖票的过程,一旦进入while第一条输出语句表示售出一张票,在多线程中若是进行就可能会出现下面的卖出重复票、错票的问题。
继承Thread的多窗口卖票问题
class MyThread extends Thread{ //继承Thread方式想要共享属性:设置为static private static int ticket = 100; @Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ break; } } } public MyThread(String name) { super(name); } } public class Main { public static void main(String[] args) throws InterruptedException { MyThread thread1 = new MyThread("线程一"); MyThread thread2 = new MyThread("线程二"); thread1.start(); thread2.start(); } }
使用继承Thread方式创建线程想要共享属性就需要设置static静态,因为都是new的同一个类的对象。
实现Runnable接口的多窗口卖票问题
class MyRunnable implements Runnable{ //实现runnable接口方式想要共享属性:默认权限 private int ticket = 100; @Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ break; } } } } public class Main { public static void main(String[] args) throws InterruptedException { MyRunnable runnable = new MyRunnable(); //使用相同的接口实现类runnable Thread thread1 = new Thread(runnable, "线程一"); Thread thread2 = new Thread(runnable, "线程二"); thread1.start(); thread2.start(); } }
在实现runnable接口类中对于想要共享的属性不需要设置static,因为该类创建的实例都作为Thread构造器参数传入,使用的是一个runnable。
问题描述以及解决方案
问题:在卖票过程中,出现了重票、错票,出现了线程的安全问题。当某个线程操作车票的过程中,尚未操作完成时,其他操作与此同时参与进来执行导致共享数据的错误。
解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中其他线程不可以参与执行。
2、同步机制(解决线程安全问题)
同步机制介绍
同步机制
同步机制:Java对于线程安全问题也出同步机制,当线程调用一个方法时在没有得到结果之前,其他线程无法参与执行。
有两种方式解决:
同步代码块:synchronized(同步监视器){ ...操作共享数据 },单独在方法中声明
同步方法:public synchronized void show (String name){ ,直接声明在方法上
synchronized介绍
synchronized锁是什么?
任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。
同步代码块:同步监视器可以是单一的对象,很多时候可以将this或类.class作为锁。
同步方法的锁:可以看到只需要在方法权限修饰前加入synchronized即可,它对应的锁是根据它的方法状态,若其方法是static静态方法(类.class);非静态方法(this)
synchronized充当角色如图:
同步锁机制:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。 —《Thinking in Java》
注意:
必须要确保使用同一个资源的多个线程共用一把锁,否则无法保证操纵共享资源的安全。
针对于同步方法中一个线程类中的所有静态方法都会共用同一把锁(类.class),非静态方法都会使用this充当锁;同步代码块一定要谨慎。
方式一:同步代码块
语法:
synchronized(同步监视器){ //需要被同步的代码 }
共享数据:多个线程共同操作的变量,例如之前的ticket。
被同步代码:操作共享数据的代码。
同步监视器:俗称锁,可以是任意一个对象,都可以充当锁,多个线程必须共用一把锁。
解决之前两种方式进行多线程的线程安全问题:
继承Thread方式
class MyThread extends Thread{ private static int ticket = 100; //这里单独创建一个静态对象 private static Object object = new Object(); @Override public void run() { while(true){ //这里使用Object静态实例对象作为锁 synchronized (object){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ break; } } } } public MyThread(String name) { super(name); } }
这里的锁是使用的类中一个静态对象实例object,对于这种继承Thread方式进行多线程的话是要new多个MyThread类的,而Synchronized中的锁必须是指定单独一把锁,所以将该object作为锁。
实现runnable接口
//解决实现runnable接口的线程安全问题 class MyRunnable implements Runnable{ private int ticket = 100; @Override public void run() { while(true){ //同步代码块:this本身指的是runnable的实现类MyRunnable synchronized (this){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ break; } } } } }
对于实现runnable方式的,我们可以直接使用this来充当锁,因为创建多个线程使用的是同一个Runnable。
注意:不管什么形式来创建多线程的,若是想要避免出现线程安全问题那么就要确保synchronized里的锁是一把锁也就是同一个对象。
方式二:同步方法
语法:
//非静态方法:锁为this public synchronized void show(){ } //静态方法:锁为类.class public synchronized static void show(){ }
解决之前两种方式进行多线程的线程安全问题:
继承Thread方式
class MyThread extends Thread{ //继承Thread方式想要共享属性:设置为static private static int ticket = 100; @Override public void run() { while(true){ if(ticket<=0 || operate()){ break; } } } //同步方法:静态方法 锁为MyThread.Class 单独一份 public synchronized static boolean operate(){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; return ticket==0; }else{ return true; } } public MyThread(String name) { super(name); } }
这里就需要使用静态方法了,此时锁为MyThread.class单独一份,若是非静态的话就是this,而下面创建多线程就是多个实例,就会出现安全问题。
实现Runnable接口方式
class MyRunnable implements Runnable{ private int ticket = 100; @Override public void run() { while(true){ //一旦ticket<=0 马上退出 if(ticket<=0 || operate()){ break; } } } //非静态方法使用this来作为锁 public synchronized boolean operate(){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; return ticket==0; }else{ return true; } } }
这里将判断操作单独抽成一个同步方法,这里是非静态的,锁就是this
注意:继承Thread方式中应使用静态方法来实现同步方法;实现runnable接口实现同步方法时,该方法是否为静态都可以,因为多个线程始终使用的是一个runnable,锁始终只有一份。
方式三:Lock锁
认识Lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制—使用显示定义同步锁对象来实现同步,同步锁使用Lock对象充当。
该Lock接口是属于java.util.concurrent.locks包,它用来控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
如何使用Lock接口呢?
我们使用其Lock接口实现类ReentrantLock类(重进入锁)。
ReentrantLock类:它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock类,可以调lock()方法显式加锁,调unlock()方法释放锁。
实际使用:解决线程同步
解决之前两种方式进行多线程的同步问题:
解决继承Thread的同步问题
class MyThread extends Thread{ //继承Thread方式想要共享属性:设置为static private static int ticket = 100; //静态实例:使用Lock锁 private static Lock lock = new ReentrantLock(); @Override public void run() { while(true){ //手动上锁 lock.lock(); if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ lock.unlock(); break; } //手动解锁 lock.unlock(); } } }
针对于继承Thread方式的lock锁在创建实例时定义为static。之后再在操作共享数据外显示上锁与解锁。
解决实现Runnable接口同步问题
class MyRunnable implements Runnable{ //实现runnable接口方式想要共享属性:默认权限 private int ticket = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while(true){ //上锁 lock.lock(); if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket); ticket--; }else{ lock.unlock(); break; } //解锁 lock.unlock(); } } }
针对于实现Runnable的情况,我们就创建一个普通实例对象即可。多个线程使用的是同一个Runnable实现类。
注意:如果同步代码有异常,要将unlock()写入finally语句块。
3、同步方法的好处及坏处
好处:解决了线程的安全问题。
坏处(局限性):在操作同步代码时,只能有一个线程参与,其他线程需要等待开锁之后才能进入,相当于是一个单线程的过程,效率低。
4、同步的范围及释放与不释放锁的操作
同步范围
对于寻找代码是否存在线程安全问题几个关键点?
明确哪些方法是多线程运行的代码。
明确多个线程是否有共享数据。
明确多线程代码中是否有多条语句操作共享数据。
解决策略:对于多条操作共享数据的语句,只能让一个线程执行完之后再让下个线程执行,即所有操作共享数据的这些语句都要放在同步范围中。对于同步范围的大小也要有个度,范围小太的话往往可能会没有锁住所有安全的问题;范围太大的话没发挥多线程的功能。
释放锁与不会释放锁的操作
释放锁的操作:
当前线程的同步方法或同步代码块执行结束会自动释放锁。
当前线程在同步方法或同步代码块中遇到break、return终止了该代码块、该方法的执行。
当前线程在同步方法或同步代码块中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步方法或同步代码块中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作:
线程执行同步方法或同步代码块时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
suspent()与resume()方法已弃用。
5、小练习
问题描述:银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打 印账户余额。
程序如下:
class Account{ private double wallet; //将该方法设置为同步方法,锁为this指的是该Account类,这里是可以的 public synchronized void deposit(double money){ if(wallet>=0){ wallet += money; System.out.println(Thread.currentThread().getName()+"向账户存储了"+money+"元,账户余额为:"+wallet); } } } class Customer extends Thread{ private Account account; public Customer(Account account,String name) { super(name); this.account = account; } @Override public void run() { for (int i = 0; i < 3; i++) { account.deposit(1000); } } } public class Main { public static void main(String[] args) throws InterruptedException { Account account = new Account(); new Customer(account,"用户A").start(); new Customer(account,"用户B").start(); } }
这里直接将Account操控数据的方法设置为同步方法,不需要使用static,因为多个线程共同操作一个Account