JAVA基础
10.多线程
1.什么是多线程?
进程:在操作系统中运行的某个软件(主要是指在内存中)。任何软件要运行都要被加载到内存中。而内存负责运行这个软件所需要的那些内存空间,就被称为当前软件在内存中的一个进程。
1.进程需要依赖于操作系统
2.运行中的程序
进程就是在操作系统中正在运行的程序。
线程:软件运行之后真正负责执行软件中具体某个功能的那个独立的内存空间(它必须位于进程中)。
1.线程需要依赖于进程。
2.线程实际上是进程中某一项功能的执行过程【执行轨迹/执行线索】
3.线程可能会在进程中有多个,至少必须有一个。
线程就是在进程某一项功能的执行过程【执行轨迹/执行线索】。
例如:
我们在window操作系统上打开“暴风影音”播放电影,此时“暴风影音”就会在window操作系统中产生一个进程;打开“暴风影音”播放电影的时候有画面,声音,中文字幕等等,这些画面,声音,中文字幕就是这个“暴风影音”进程中的多个线程。
1.没有进程就没有线程。【线程需要依赖与进程】
2.线程可能会在进程中有多个,至少必须有一个。
多线程:某一个程序在运行的时候【进程】可能会产生多个不同的执行线索【执行轨迹】【线程】,这些多个不同的执行线索【执行轨迹】共同运行的情况就是多线程。往往我们会感觉到这些多个不同的执行线索【执行轨迹】同时执行,实际上这时一种错觉假象,实际上当这些多个不同的执行线索【执行轨迹】在运行的时候,某一个时刻只用一个执行线索【执行轨迹】在运行,只是这多个不同的执行线索【执行轨迹】快速的切换而已。
“暴风影音”播放电影的时候,我们感觉图像和声音在同时运行,实际上你被骗了,因为程序在执行的时候,某一个时刻只能有一条线程运行,那么为啥你感觉图像和声音在同时运行,因为进程控制线程快速的切换导致,我们感觉图像和声音在同时运行。
例如:《超体》
注意:
CPU在某个时间点上其实它只能执行一个线程。但是由于CPU在执行线程的过程中会随时切换到其他的线程上。
这样导致我们看到的效果类似于多个线程在同时运行。CPU随机在多个线程之间进行切换。
上面介绍的这种CPU的执行原理其实是针对的单核单线程的CPU。【单行道】
现在的CPU基本都是双核四线程,四核(四线程)八线程。【双向8车道】
由于CPU在多个线程之间会进行高速的切换。导致我们误认为多个线程同时运行。
2.多线程的创建方式以及区别?
1.一个java程序启动运行以后,至少有2个线程要运行。
1.主线程—main线程【主方法中java代码的执行过程】
public static void main(String args[]){
Java 代码
}
2.垃圾回收线程。【垃圾自动回收机制】
2.线程操作中常用的类和接口
Thread类
java.lang 类 Thread【程序中的执行线程】【不需要导包】
public class Thread extends Object implements Runnable
如果我们要想在JAVA中使用线程,就必须通过Thread类完成。
如何通过Thread类创建一个线程
1.新建一个java类,继承Thread类。
2.重写run方法
3.将需要由线程执行动作,写入run方法
例如:
package com.click369.test1;
public class MyThread extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("MyThread=="+i);
}
}
}
如何运行通过Thread类创建的线程?
1.创建线程对象
2.通过线程对象调用继承自Thread类的start方法启动线程运行。
package com.click369.test1;
public class ThreadTest {
public static void main(String[] args) {
//1.创建线程对象
MyThread th1=new MyThread();
MyThread th2=new MyThread();
//直接调用run方法执行不是启动线程运行的方式。
//直接调用run方法执行实际上是对象调用方法运行的过程。
//th1.run();
//th2.run();
//2.通过线程对象调用继承自Thread类的start方法启动线程运行。
//启动线程运行是需要使用Thread类的start方法。
//Thread类的start方法会触发run方法运行。
th1.start();
th2.start();
}
}
Runnable接口
java.lang 接口 Runnable
public interface Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
方法摘要 | |
---|---|
void | run() 使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。 |
Thread类与Runnabl接口的关系
Thread类实现过Runnabl接口,Thread类是Runnabl接口的子类。
理解Thread类中的run方法,实际上是来自Runnabl接口
如何通过Runnable 接口创建一个线程?
1.新建java类,实现Runnable 接口。
2.重写run方法
3.将需要由线程执行动作,写入run方法
例如:
package com.click369.test1;
public class TestRun implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("TestRun=="+i);
}
}
}
如何运行通过Runnable 接口创建的线程?
由于启动线程需要使用Thread类中的strat方法,可是我们现在通过实现Runnable 接口创建的线程是没有start方法,我们就需要将通过实现Runnable 接口创建的线程与Thread类产生关联才能使用Thread类中的strat方法去启动线程运行。
完成实现Runnable 接口创建的线程与Thread类产生关联的就是Thread类的构造方法:
构造方法摘要 |
---|
Thread() 分配新的 Thread 对象。 |
Thread(Runnable target) 分配新的 Thread 对象。 |
Thread(Runnable target, String name) 分配新的 Thread 对象。 |
Thread(String name) 分配新的 Thread 对象。 |
我们发现
Thread(Runnable target)/Thread(Runnable target, String name) Thread类的构造方法与Runnable 接口有关联。
启动线程需要使用Thread类中的strat方法,所以我们要创建Thread类对象,创建Thread类需要构造方法
Thread(Runnable target)/Thread(Runnable target, String name) 的第一参数是Runnable接口类型,这时当一个
方法的参数是接口类型的时候,我们可以传递实现该接口的子类对象,而上面的TestRun 类刚好实现过Runnable接口,这时我们可以将TestRun 类的对象传递给Thread类的构造方法,就达成了实现Runnable 接口创建的线程与Thread类产生关联的这个目的。
步骤:
1.创建实现Runnable 接口创建的线程类的对象【目标对象】
2.创建Thread类的对象【线程对象】,将目标对象传递给Thread类的构造方法
3.通过线程对象调用start方法启动线程运行。
例如:
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//1.创建实现Runnable 接口创建的线程类的对象【目标对象】
TestRun tr=new TestRun();
//2.创建Thread类的对象【线程对象】,将目标对象传递给Thread类的构造方法
Thread th1=new Thread(tr);
Thread th2=new Thread(tr);
//3.通过线程对象调用start方法启动线程运行。
th1.start();
th2.start();
}
}
上面我们介绍了两种常用的创建线程的方式,那么这个两种创建线程的方式构造起来的线程到底有什么区别?
通过继承Thread类创建的线程类 与 通过Runnable 接口创建的线程类的区别?
1.创建方式:extends Thread,impements Runnable ;
2.启动方式不同
extends Thread,直接线程对象【继承Thread类的子类对象】调用strat方法启动运行
impements Runnable,先创建目标对象【实现Runnable接口的子类】,创建线程对象【Thread类的对象{Thread(Runnable target)/Thread(Runnable target, String name}】,将目标对象作为参数传递给创建线程对象的Thread类的构造方法,然后通过线程对象的strat方法启动运行。
3.extends Thread创建的线程是不能实现数据共享功能的,impements Runnable创建的的线程是可以实现数据共享功能的。【了解,后面【实现线程同步的时候】我们会通过实例来测试这个区别】
3.线程的常用方法?
Java中线程的常用方法—Thread类的常用方法
void | start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
---|---|
static | Thread currentThread()返回对当前正在执行的线程对象的引用。 |
String | getName()返回该线程的名称。 |
void | setName(String name)改变线程名称,使之与参数 name 相同。 |
线程名名称,java程序运行以后会给程序中的每一个线程一个名称,默认的主线程的名称是”main”.,非主线程的其他线程名称的默认名称都是“Thread-0”“Thread-1”,“Thread-2”,以此类推。
例如:
package com.click369.test1;
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName("线程1");
th2.setName("线程2");
th1.start();
th2.start();
得到/设置线程的优先级
int | getPriority() 返回线程的优先级。 |
---|---|
void | setPriority(int newPriority) 更改线程的优先级。 |
线程的优先级:就是线程的执行先后。
因为java中的线程的调用执行是jvm中的线程调度器负责管理线程,在java中线程的执行先后,我们是控制不了的。
我们无法控制线程的执行次序,但是我们可以通过设置线程的优先级,控制线程优先执行的几率。
优先级数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级大谁就一定先执行。
我们将线程的优先级分为10个级别【1–10】,数字越大优先级越高。
Java还为我们提供了3个静态常量,用于保存
static int | MAX_PRIORITY 线程可以具有的最高优先级。10 |
---|---|
static int | MIN_PRIORITY线程可以具有的最低优先级。1 |
static int | NORM_PRIORITY分配给线程的默认优先级。5 |
默认java为每一个线程设置的优先级都是一样的级别为5【NORM_PRIORITY】
我们可以通过setPriority来修改线程的优先级,数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级谁就一定先执行。
注意:绝对不能用修改线程的优先级,来确保某一个线程就要先执行。
例如:
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName("线程1");
th2.setName("线程2");
th1.setPriority(1);
th2.setPriority(10);
int pri1=th1.getPriority();
int pri2=th2.getPriority();
System.out.println("第一个线程的优先级是=="+pri1);
System.out.println("第二个线程的优先级是=="+pri2);
th1.start();
th2.start();
守护线程
boolean | isDaemon() 测试该线程是否为守护线程。 |
---|---|
void | setDaemon(boolean on) 将该线程标记为守护线程用户线程。 |
守护线程:我们也叫精灵线程,普通的线程又叫用户线程。
当所有用户线程都执行完毕以后,无论守护线程能否继续运行,都要立刻停止运行。【共死】
package com.click369.test2;
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
package com.click369.test2;
public class MyThread2 implements Runnable {
@Override
public void run() {
while(true){
System.out.println("守护线程!!!!");
}
}
}
package com.click369.test2;
public class ThreadTest {
public static void main(String[] args) {
MyThread my=new MyThread();
Thread th1=new Thread(my);
Thread th2=new Thread(my);
MyThread2 my2=new MyThread2();
Thread th3=new Thread(my2);
//void setDaemon(boolean on) 将该线程标记为守护线程用户线程。
th3.setDaemon(true);
// boolean isDaemon() 测试该线程是否为守护线程。
//System.out.println("th1--守护线程?--"+th1.isDaemon());
//System.out.println("th2--守护线程?--"+th2.isDaemon());
//System.out.println("th3--守护线程?--"+th3.isDaemon());
th1.start();
th2.start();
th3.start();
}
}
static void | sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
---|---|
void | interrupt() 中断线程。 |
例如:
package com.click369.test3;
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+"开始运行!!");
//static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
System.out.println("休眠100秒的时候被打断");
}
System.out.println(name+" i="+i);
}
}
}
package com.click369.test3;
public class ThreadTest {
public static void main(String[] args) {
MyThread my=new MyThread();
Thread th1=new Thread(my);
th1.start();
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+"开始运行!!");
//static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("休眠的时候被打断");
}
if(i==10){
th1.interrupt();
}
if(i==20){
th1.interrupt();
}
if(i==30){
th1.interrupt();
}
System.out.println(name+" i="+i);
}
}
}
例如:
package com.click369.test4;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadTest {
public static void main(String[] args)throws Exception {
BufferedReader read=new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入闹钟时间:");
String naozhongtime=read.readLine();
boolean flag=true;
while(flag){
Thread.sleep(1000);
SimpleDateFormat sdf=new SimpleDateFormat("HH:mm:ss");
String newTime=sdf.format(new Date());
System.out.println(newTime);
if(newTime.equals(naozhongtime)){
System.out.println("闹钟时间到,开始响铃!!!");
flag=false;
}
}
}
}
void | join(long millis)等待该线程终止的时间最长为 millis 毫秒。【强制线程执行】 |
---|
package com.click369.test5;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadTest {
public static void main(String[] args)throws Exception {
MyThread my=new MyThread();
Thread th1=new Thread(my);
th1.start();
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+"开始运行!!");
System.out.println(name+" i="+i);
if(i==20){
th1.join(5000);
}
}
}
}
4.线程的生命周期?
1、线程的生命周期就是线程从一开始创建,到run方法执行完毕以后的状态变化。[状态之间的切换]
2、线程的生命周期几种状态【1、新建状态 2、就绪状态 3、运行状态 4.阻塞状态 5.死亡状态】
创建状态:通过new的方式创建出线程对象,此时线程就进入到创建状态【新建状态】。
新建状态的线程是不能运行。
就绪状态:新建状态的线程调用strat方法之后就会进入就绪状态。
就绪状态的线程具备执行能力,但是没有cpu资源。【万事具备只差cpu】.
运行状态:就绪状态的线程得到cpu资源开始执行run方法,此时这个线程就是运行状态。
运行状态的线程当cpu切换到其他线程时候,本线程就再一次进入就绪状态。
阻塞状态:运行状态的线程调用sleep/wait方法…此时线程进入阻塞状态。
处于阻塞状态的线程的休眠时间到/调用notify方法/notifyAll方法在此时线程进入就绪状态,从就绪状态中得到cpu资源从而进入运行状态。
死亡状态:运行状态的线程run方法运行结束/线程执行过程中遇到异常/调用stop方法此时线程就进入到死亡状态。
死亡状态的线程是不能运行,除非再一次使用strat方法重新启动运行。
5.为什么需要线程同步/线程安全?什么是线程同步/线程安全?线程同步/线程安全实现方式有几种,它们有什么区别?
线程同步
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
@Override
public void run() {
//1.通过while循环控制买票的持续性
//定义一个变量,来控制while循环
boolean flag=true;
while(flag){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
}
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//创建目标对象
MyThread my=new MyThread();
//创建线程对象【实际上就是买票的窗口】
//开3条线程卖出5张票
Thread th1=new Thread(my);
Thread th2=new Thread(my);
Thread th3=new Thread(my);
//为线程线程设置名称
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
//启动线程开始买票
th1.start();
th2.start();
th3.start();
}
}
运行结果:
上面的程序在买票的时候,卖还剩-1张票的结果,显然这个结果是不符合逻辑。
当窗口3卖出一张票以后,还剩1张票的时候,此时线程切换给了窗口1,窗口1线程就先检查有没有可以卖出的票,此时得到的结果是还剩1张,这是窗口1就开始收钱,打印票据,当窗口1在收钱/打印票据时候,线程就切换给了窗口2,这是创建开始判断是否有票可买的时候,由于当窗口1在收钱/打印票据,还没有对票数进行减1的操作,因此窗口2判断有一张票可买,窗口2开始收钱,打印票据,找钱完成以后,对票数进行减1操作以后,票数已经为0 ,还没有是输出卖出1张票的结果的时候,线程又一次切换给窗口1,窗口1是在窗口2对票数进行减1操作之后,再一次对票数进行的减1操作,所以创建就会出现还剩-1张票的情况。
原因是因为CPU在执行某个线程的时候,并没有将线程的任务全部执行完成就切换到其他的线程上导致数据有误。
原因:多条线程去访问同一个资源的时候,可能会出现资源访问数据不一致的情况,为了避免这种情况的出现,就需要使用线程安全【线程同步】。
线程安全问题
线程安全问题发生本质:多个线程他们在操作共享的数据(资源)。而CPU在执行线程的过程中操作共享资源的代码还没有彻底执行完,CPU就切换到其他线程上,导致数据不一致。
为什么?
原因:多条线程去访问同一个资源的时候,可能会出现资源访问数据不一致的情况,为了避免这种情况的出现,就需要使用线程同步【线程安全】。
解决安全问题:线程的同步技术。
同步的目的就是保证有一个线程在执行任务代码的时候,其他线程要在外面等待。
同步原理:只要某些代码(牙医)被添加了同步(门,应该门上的那个锁),那么任何线程在进入被同步控制的代码的时候都需要判断有没有其他某个线程在同步中(要想看牙医,需要先能够打开锁),如果有当前其他的任何线程都需要在同步的外面等待,如果没有这时只有某一个线程可以进入到同步中,其他线程就继续在同步的外面等待。
同步原理:当窗口1去访问票数的时候,我们使用线程同步技术,将被访问的票数锁定,如果窗口2要想访问票数,窗口2就得先判读是否有其他的窗口正在使用这个票数,如果判断有其他的窗口正在使用这个票数,那么窗口2就需要去等待,等待窗口1对票数资源访问完毕以后,再去访问这个票数,窗口2在访问票数时候,我们同样需要使用线程同步将被访问票数资源锁定,其他的线程要想访问票数资源,就要等到窗口2对票数资源访问才可以访问票数资源。
是什么?
【线程安全/线程同步】:多条线程去访问同一个资源的时候,每一次保证一条线程正常访问资源,当前该线程访问资源的时候,其他的线程就需要等待,当前该线程访问资源结束之后,允许另一条线程去访问资源,其他线程继续等待。【排队看牙医】
Java中实现线程同步的方式
1.同步代码块
同步代码块的书写格式:
synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
例如:
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
@Override
public void run() {
//1.通过while循环控制买票的持续性
//定义一个变量,来控制while循环
boolean flag=true;
while(flag){
/**
* 同步代码块格式
* synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
*synchronized---同步关键字
*(同步对象)--需要被锁定的资源所在类的对象
*{}---【块】
*将需要同步的Java程序写上面的{}块中
*/
synchronized(this){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
}
}
上面我们通过同步代码块实现线程同步,同步代码块在编写的时候,需要设置一个同步对象,很多人都不明白这个同步对象都是是谁,不容易被判定。
能不能有一个不需要判定同步对象的这个线程同步的实现方式-----同步方法
2.同步方法
同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
被synchronized 关键字修饰的方法就是同步方法
例如:
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
//定义一个变量,来控制while循环
private boolean flag=true;
@Override
public void run() {
//1.通过while循环控制买票的持续性
while(flag){
//调用同步方法的执行
seller();
}
}
/**
* 创建买票的同步方法
* 同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
*/
private synchronized void seller(){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
运行结果:
3.使用JDK5中的Lock接口取代同步代码块
JDK5中的Lock接口
在JDK5版本之前:解决多线程的同步问题使用同步代码块。在JDK5之后,提供另外一个接口Lock。它可以代替同步代码块。
java.util.concurrent.locks 接口 Lock
public interface Lock
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
Lock接口,它实现了比同步代码块更加方便的同步操作。Lock接口中提供由程序员自己手动调用方法来获取同步锁和释放同步锁。
同步代码块获取锁和释放锁都是隐式看不见的。
void lock()获取锁。
void unlock()释放锁。
使用java.util.concurrent.locks.ReentrantLock类创建Lock接口的对象。
例如:
package com.click369.test1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
//定义一个变量,来控制while循环
private boolean flag=true;
//创建一个Lock接口对象[接口回调对象]
private Lock lock=new ReentrantLock();
@Override
public void run() {
//1.通过while循环控制买票的持续性
while(flag){
//锁定资源
lock.lock();
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
//释放资源锁定
lock.unlock();
}
}
}
前面我们在介绍线程的创建方式的是时候:通过继承thread类创建线程没有数据共享功能,通过实现Runnable接口构造的线程有数据共享功能.
6.sleep 与wait的区别?
区别:
- sleep()方法,是属于Thread类中的;
wait()方法,则是属于Object类中的。 - sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。(在调用sleep()方法的过程中,线程不会释放对象锁。)
wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。(获取对象锁进入运行状态。) - wait()方法需要释放锁,前提条件是它已经持有锁。所以wait和notify(或者notifyAll)方法都必须被包裹在synchronized语句块中,并且synchronized后锁的对象应该与调用wait方法的对象一样。否则抛出IllegalMonitorStateException
- sleep()方法告诉操作系统至少指定时间内不需为线程调度器为该线程分配执行时间片,并不释放锁(如果当前已经持有锁)。实际上,调用sleep方法时并不要求持有任何锁。
7.notify 与notifyAll的区别?
锁池:
假设线程A已经拥有对象锁,线程B、C想要获取锁就会被阻塞,进入一个地方去等待锁的等待,这个地方就是该对象的锁池;
等待池:
假设线程A调用某个对象的wait方法,线程A就会释放该对象锁,同时线程A进入该对象的等待池中,进入等待池中的线程不会去竞争该对象的锁。
notify和notifyAll的区别:
1、notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会;
2、notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;