Thread线程
PS:现在没有单片机【单核CPU】,接下来在你电脑看到所有结果都是对
为什么要有线程?
PS:Java程序的组成【顺序,分支,循环】
需求:在这里设计一个程序既可以打游戏又可以听音乐?
对于在的需求,解决方案有两种
1.以进程方式实现打游戏和听音乐
2.以线程方式实现打游戏和听音乐
什么是进程和什么是线程?
进程:是指一个内存中运行的**【应用程序】,每个进程都有字节的独立的一块内存区域,一应用程序可以同时【启动多个线程】**
PS:一个进程中可以包含多个线程
【例如: 百度网盘有下载任任务【百度网盘是是进程,而它的下载是线程(多线程)】】
如果使用进程解决打游戏和听音乐是可以?
完全可以可以,可以开发一个进程听歌,也可以开发一个进程玩游戏,进程和进程之间的通信不是很方便,并且若是在某个进程内部完功能,就不建议以进程的形式开发【因为启动一个功能就相当于开启进程,就相当于执行了以应用程序,并且彼此之间是独立】
此时需要在进程的内部同时完成这个功能,此时就可以选用**【线程】**
线程:是指**【进程】中的一个【执行任务单元(控制单元)】,一个进程可以同时【并发运行多个线程】**
例如:【IDEA Java编译器在这个编译器中(进程),存在多个线程帮助IDEA进行进程管理
(例如:改错,报错)】
一个进程至少有一个线程,为了提高效率,可以在一个进程中启动多个线程即**【多线程】**
进程和线程的区别
进程:是有自己独立内存空间的,进程中的数据存放空间(堆栈区域)是独立的,至少有一个线程
线程:堆空间是共享的,栈空间是独立的,线程消耗资源比进程要小,相互之间可以互相影响**【线程通信、同步和异步线程】,所以称线程为【轻型的进程或进程元】**
PS:开发线程成本要远远低于开发进程,因为一个进程中有多个线程并发执行,那么从微观的角度而言是可以解析出(考虑出)先后执行顺序,那么那个线程先执行,那个线程后执行是可以思考出来,但是在实际的执行中是不可以预测【线程执行完全取决于CPU的调用,CPU会分发时间片和回收时间片】,程序源可以进行干预,可以限制或修改或控制线程获取CPU时间片,以达到对线程的控制
多线程并发访问的时候可以看做时瞬间抢夺CPU时间片,谁抢到谁先执行,这就造成了线程的随机性
并发和并行
PS:并发和并行是即相似又有区别(微观的角度)
并行:指两个或多个事件在**【同一个时刻点】**发生
并发:指两个或多个事件在**【同一个时间段内】**发生
在操作系统中,在多道程序环境下,并发性是指在一段时间内宏观上有**【多个程序在同时运行】**,但在单CPU系统中,每一时刻却仅能有一道程序执行(时间片),故微观上这些程序只能是【分时地交替执行】。
**倘若计算机系统中有多个CPU,则这些可以并发执行的程序便可被分配到多个处理器上,实现多任务并行执行,**即利用每个处理器来处理一个可并发执行的程序,这样,多个程序便可以同时执行,因为是微观的,所以大家在使用电脑的时候感觉就是多个程序是同时执行的。
所以,大家买电脑的时候喜欢买“核”多的,其原因就是“多核处理器”电脑可以同时并行地处理多个程序,从而提高了电脑的运行效率。
单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。
同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个进程的运行,当系统只有一个CPU时,进程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
线程的执行都需要时间片即CPU分配给各个程序的运行时间.
多线程优势:
多线程作为一种多任务、并发的工作方式,当然有其存在优势:
① 进程之前不能共享内存,而线程之间共享内存(堆内存)则很简单。
② 系统创建进程时需要为该进程重新分配系统资源,创建线程则代价小很多,因此实现多任务并发时,多线程效率更高.
③ Java语言本身内置多线程功能的支持,而不是单纯第作为底层系统的调度方式,从而简化了多线程编程.
Java中如何实现线程
创建线程一共三种方式:先学习前两种
1.继承Thread类: 此时的类就是一个线程类,这个类必须实现run方法,run方法就是线程执行的核心
2.实现Runnable接口: 此时这个类就不是一个线程类,它只是一个是实现了一个接口的类,这个类也必须实现接口中的run方法,run方法就是线程执行的核心
-
-
返回值类型 此方法是实现线程的核心操作 void
run()
当一个对象实现的接口Runnable
是用来创建一个线程,启动线程使对象的run
方法在单独执行的线程调用。
-
方式一:继承Thread类
1.先创建一个类然后继承Thread类
2.在子类中必须重写run方法【run方法的实现体就是当前线程主体】
3.在测试类的main方法中,创建线程并启动
创建线程对象的两种方式:
1. 继承Thread的子类 线程对象名 = new 继承Thread类();
PS: 因为是继承Thread类所以,它是子类会有父类中方法,所以这个类也被成线程类
2. Thread 线程对象名 = new 继承Thread类();
PS: 对象的向上转型,还是一个完整线程类
切记,切记,切记!!启动线程一定是 调用start方法 ,而不是调用run方法
线程对象.start();
package com.qfedu.Thread;
//音乐的线程类 MusicThread就是一个线程类
public class GameThread extends Thread {
//必须重写父类Thread的run方法,run方法是线程的核心
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("打游戏:"+i);
}
}
}
//音乐的线程类 MusicThread就是一个线程类
public class MusicThread extends Thread {
//必须重写父类Thread的run方法,run方法是线程的核心
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("播放音乐:"+i);
}
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
//Java中存在两个线程【这两个线程是必须有的】
//一个是main入口方法即主线程【主线程的优先级高于一切子线程(自己创建)】
//另外一个线程是GC垃圾回收机制【开启了一个守护线程,这个线程守护当前执行代码当代码执行完毕之后,进行空间回收】/
//在main方法中启动线程
MusicThread thread1 = new MusicThread();//线程对象就创建好了,这是通过继承Thread类完成操作
Thread thread2 = new GameThread();//创建好了线程对象,此时是完成了向上转型,所以可以进行线程操作
//线程的执行结果是不可预测【但是可以大范围猜测到部分执行结果】,但是可以强加干预,以达到预期效果
thread1.start();
thread2.start();
}
}
方式二:实现Runnable接口
PS:实现Runnable接口并不是一个线程类,这个类只是实现了Runnable接口,需要借助Thread类,来完成线程的创建
1. 创建一个普通类实现Runnable接口
2. 在普通类中必须实现run方法
3. 借助Thread类创建线程对象,以启动线程
切记,切记,切记!!启动线程一定是 调用start方法 ,而不是调用run方法
线程对象.start();
-
-
Thread(Runnable接口实现类的对象)
分配一个新的Thread
对象。Thread(Runnable接口实现类的对象, String 当前线程的名字)
分配一个新的Thread
对象。
-
创建对象:
Thread 线程对象 = new Thread(Runnable接口实现类的对象);
或
Thread 线程对象 = new Thread(Runnable接口实现类的对象,当前线程的名字);
代码实现
package com.qfedu.Runnable;
//此类是实现Runnable接口
public class GameThread implements Runnable {
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("打游戏:"+i);
}
}
}
//此类是实现Runnable接口
public class MusicThread implements Runnable {
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("播放音乐:"+i);
}
}
}
package com.qfedu.Runnable;
//测试类操作
public class RunnableTest {
public static void main(String[] args) {
//创建实现Runnable接口类的线程对象
Thread thread1 = new Thread(new GameThread());
//创建线程对象的同时,指定线程的名字
Thread thread2 = new Thread(new MusicThread(),"听音乐");
thread1.start();
thread2.start();
}
}
Thread类和Runnable接口的匿名实现类方式(了解)
PS:当前线程仅限在当前类中的时候用,这种情况下就可以使用匿名内部类的形式完成
package com.qfedu.AnonymityThread;
//这些操作方式仅限 线程使用一次,若线程多次使用【必须创建类的实现】
public class AnonymityThreadDemo {
public static void main(String[] args) {
//此方式仅限使用一次【匿名内部类】
new Thread(){
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("听音乐:"+i);
}
}
}.start();
//实现Runnable接口【匿名内部类】
new Thread(new Runnable() {
@Override
public void run() {
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("打游戏:"+i);
}
}
}).start();
//lambda表达式的Runnable接口实现
new Thread(()->{
//run的方法方法体就是线程执行的核心【你要干什么】
//启动做什么操作
for(int i= 0;i<50;i++){
System.out.println("打酱油:"+i);
}
}).start();
}
}
start方法和run方法的区别
1) start:
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
2) run:
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。
Thread类创建线程和Runnable创建线程的区别
两者都要实现run方法,使用Thread那么需要继承,并重写父类中的run方法**【当前类是线程类】,实现Runnable接口,就需要实现接口的run方法【不是线程类是普通实现类】**,从扩展角度而言,最好使用Runnable接口,若能确定只是一个线程的调用那么我们使用Thread,若是需要继承一个父类在调用线程并进行扩展推荐使用Runnable。
实际项目中推荐大家使用Runnable接口
class A extends Thread{}
class A extends B implement Runnable,XXXInteface{} --》 这种样式使用最多
线程中常用方法
线程状态
PS:在API中标注错误,State是一个枚举并不是一个类,但是不影响使用
public enum State {
/**
* 出生,新生
* 表示线程尚未启动正在等待创建
*/
NEW,
/**
* 准备就绪和执行
当前线程正在准备就绪状态等待执行
*/
RUNNABLE,
/**
* 休眠
线程【阻塞状态】,此时当前编程会让出CPU时间片,等待继续执行【sleep】
*/
BLOCKED,
/**
等待
无限期的等待,等待另外一个线程执行操作【wait】
*/
WAITING,
/**
时间等待
无限期等待,等待另外一个线程执行操作【这个等待是有时限限制的,如果在规定时间内没有唤醒,到时间后也会自动醒来】 【wait和sleep】
*/
TIMED_WAITING,
/**
消亡,死亡
线程执行完毕之后会出现这个状态【这个状态很少能观测到】
*/
TERMINATED;
}
代码
package com.qfedu.State;
//getState()方法可以获取到当前线程的状态
public class StateDemo extends Thread{
@Override
public void run() {
System.out.println("进入run方法当前线程的状态:"+this.getState());
int sum = 0;
for(int i = 0;i<=100;i++){
sum += i;
}
System.out.println("sum = "+sum);
System.out.println("结束run方法当前线程的状态:"+this.getState());
}
public static void main(String[] args) {
//1. 创建线程对象
Thread thread = new StateDemo();
System.out.println("启动线程前的状态:"+thread.getState());
thread.start();
System.out.println("start方法之后线程的状态:"+thread.getState());
System.out.println("我是一个无关紧要的执行---------");
System.out.println(thread.getState());
}
}
设置线程优先级
PS:优先级有机率提高线程获取CPU时间片的概率,理论上优先接越高,那么获取CPU时间片的机率越大,反之越小【高优先级的会享有优先执行权】
优先级相当于是给CPU一共暗示【先由我这个线程开始】,但是实际的决定权还是在争抢CPU时间片
所有创建好的线程由一个默认优先级**【级别统一都是5】**
线程优先级的范围 ,从1开始到10结束【前后都包含】,优先级为1等级最低 ,优先级为10等级最高
-
-
数据类型int 系统设置好是三个静态常量 static int
MAX_PRIORITY
线程可以拥有的最大优先级。 【10】static int
MIN_PRIORITY
线程可以拥有的最小优先级。 【1】static int
NORM_PRIORITY
被分配给线程的默认优先级。 【5】
-
修改优先接可以使用提供方法 setPriority(参数是一个int类型范围是从1开始到10结束)
package com.qfedu.Priority;
//修改线程优先级
public class SetPriorityDemo extends Thread{
@Override
public void run() {
for (int i = 0;i<100;i++){
//getName() 可以获取当前线程的名字
//线程名字的组成 是 Thread-数字 ,从0开始根据创建线程的多少进行递增
System.out.println("线程:"+getName()+"---------"+i);
}
}
public static void main(String[] args) {
Thread thread1 = new SetPriorityDemo();
Thread thread2 = new SetPriorityDemo();
//先不设置线程优先级getPriority()获取线程优先级 默认优先级都是5
// System.out.println(thread1.getPriority());
// System.out.println(thread2.getPriority());
//修改线程优先级 优先级的差值越大效果相对比较明显,优先级的差值越小效果越不明显
thread1.setPriority(Thread.MAX_PRIORITY); //和直接传递10效果是一样
thread2.setPriority(Thread.MIN_PRIORITY);//和直接传递1效果一样
thread1.start();
thread2.start();
}
}
更改线程的名字
线程是有默认名字【Thread-数字】,数字是从0开始,随着线程增多递增,随着线程减少而递减
为了更加有效的区分线程,提供了三种秀海线程名字的方式
1.使用线程对象.setName("线程名字")
2.实现Runnable接口,在创建线程对象的时候指定线程的名字 new Thread(Runnable接口实现类对象,"线程名字") 3.继承Thread类,提供一个参数的构造方法,这个参数是线程名字,在对象时调用这个构造方法进行对名字初始化
PS:获取名字,使用getName()方法
package com.qfedu.SetName;
public class SetNameDemo {
public static void main(String[] args) {
new ThreadA("我叫ThreadA").start();
//通过外部设置线程名字
ThreadB threadB = new ThreadB();
threadB.setName("我叫ThreadB");
threadB.start();
new Thread(new ThreadC(),"我叫ThreadC").start();
}
}
class ThreadA extends Thread{
//继承Thread类,提供有参构造方法添加线程名字
public ThreadA(String name){
super(name);
}
@Override
public void run() {
System.out.println("线程名:"+getName());
}
}
class ThreadB extends Thread{
@Override
public void run() {
System.out.println("线程名:"+getName());
}
}
class ThreadC implements Runnable{
// 实现Runnable接口的类不是线程类,只是实现Runnable接口中run方法以便提供给Thread使用
@Override
public void run() {
//在Runnable接口中获取当前线程的名字,需要使用到一个静态方法这个方法可以获取当前线程实例【对象】
//currentThread() 在什么位置调用就代表什么哪个线程对象
System.out.println("线程名:"+Thread.currentThread().getName());
}
}
线程休眠
线程休眠:让执行的线程暂停一段时间,进入计时等待状态
-
-
static void
sleep(long millis)
当前正在执行的线程休眠(暂停执行)为指定的毫秒数。毫秒和秒之间的换算 1000毫秒 == 1秒 这个方法存在一个编译时异常InterruptedException【中断异常】这个异常时编译时异常,在写代码的时候就需要处理 sleep方法的执行过程
当线程中调用sleep方法之后,当前线程会放弃CPU(时间片),在指定时间段内,sleep所在的线程就不会获取到执行的机会
特别注意:【在同步锁(对象锁/同步监听器)中sleep不会释放CPU时间片】
需求:实现一个交替打印输出的效果,做一个线程,线程的执行逻辑是当%5整除的时候进行休眠,在主线程中也执行这个效果,
-
package com.qfedu.ThreadMethod;
//sleep方法
public class SleepDemo extends Thread{
@Override
public void run() {
for (int i = 1;i<=100;i++){
if(i%5==0){
System.out.println("当前线程"+getName()+"~~~~~~~~"+i);
//线程中异常不建议抛出,建议内部处理,快速生成try-catch等 代码块ctrl+atl+t
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread thread = new SleepDemo();
thread.setName("子线程");
thread.start();
for (int i = 1;i<=100;i++){
if(i%5==0){
//主线程休眠
System.out.println("当前线程名字:"+Thread.currentThread().getName()+"~~~~~~~~"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
PS:现阶段sleep方法无法做到线程的交替执行,线程中有sleep方法会释放cpu时间片【线程休眠】,但是线程醒来之后会继续争抢CPU时间片
特别注意:【同步锁(对象锁/同步监听器)】这些范围内sleep方法是不会释放cpu时间片
线程礼让
PS:这个方法的效果不是特别明显
yield方法:若在线程体中,调用该方法,但不线程会给CPU发出同一个暗示【不急着运行】,可以将本线程占有的CPU时间片进行回收,分配给线程使用
PS:至于是否CPU立即回收时间片,取决于CPU,CPU即可以立即回收也可以不回收【CPU忽略提示】
需要注意:调用该方法之后,线程对象会进入到就绪状态【等待分配CPU时间片】,所以这里完全可能出现,某个线程调用了yield方法之后线程调用器又把它重新执行起来。
package com.qfedu.ThreadMethod;
//线程礼让
class EThread extends Thread{
@Override
public void run() {
for(int i = 1 ;i<=10;i++){
System.out.println("线程的名字:"+getName()+"~~~~~~~"+i);
if (i%2 == 0){
Thread.yield();
}
}
}
}
public class YieldDemo {
public static void main(String[] args) {
Thread a = new EThread();
a.setName("a线程");
Thread b = new EThread();
b.setName("b线程");
a.start();
b.start();
}
}
sleep方法和yield方法的区别
1.都能使当前线程处于放弃CPU时间片,把运行机会给其他线程
2.sleep方法会给其他线程运行机会,但是不考虑【其他线程优先级】yield方法只会给相同优先级或优先级更高的线程运行机会
3.调用sleep方法后,线程进入的计时等待状态【当前状态完毕之后是准备就绪状态】,调用yield方法后,线程进入的准备就绪状态
线程合并
join(合并):将一个正在处于运行状态的线程,强制进入得到等待状态【让出CPU时间】,让加入到这个线程执行的另外一个线程执行,原有线程只能等待加入线程执行完毕之后才可以继续执行
package com.qfedu.Join;
public class MeiZi extends Thread {
@Override
public void run() {
for(int i = 1 ;i<=50;i++){
System.out.println("妹子在看爬山"+"第【"+i+"】集");
if(i == 10){
//添加其他线程执行
//join方法原则必须是在start方法之后,需要处理异常【编译时异常】
HaiZi hz = new HaiZi();
hz.start();
try {
hz.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class HaiZi extends Thread {
@Override
public void run() {
for(int i = 1;i<=50;i++){
System.out.println("汉子在看被爬山"+i+"集");
}
}
}
public class JoinTest {
public static void main(String[] args) {
MeiZi mz = new MeiZi();
mz.start();
}
}
特点:
1.线程合并,当前线程一定会释放CPU时间片,CPU会将时间片分配给要Join进来线程【谁调用Join,谁就那个线程】
2.在加入线程没有执行完毕之前,原始线程是不会执行的
3.join执行之前,要加入的线程是处于准备就绪状态【start】
守护线程【后台线程】
后台线程又称为【守护线程】,JVM中垃圾回收机制是一个典型后台线程
特点;
若所有的前台线程都死亡,后台线程会自动死亡。若前台线程没有结束,后台线程就不会结束。
如果设置后台线程,线程对象.setDaemon(true) 【参数是一个boolean值,这个值true就是守护线程 false就不是】
检查档案线程是都是守护线程 线程对象.isDaemon() 返回值是boolean true就是 false 就不是
PS:线程开启来访问,需要连接一些物理【磁盘文件等】或网络资源【tcp,网络通信】,释放他们的资源
package com.qfedu.SetDeamon;
public class SetDeamonDemo extends Thread {
@Override
public void run() {
for(int i = 0;i<=50;i++){
System.out.println("当前线程的名字:"+getName()+"-----"+i);
//为了能看到效果让其sleep一会
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread thread1 = new SetDeamonDemo();
thread1.setName("守护线程");
thread1.setDaemon(true);
thread1.start();
//当前守护的是主线程
for(int i = 0;i<10;i++){
System.out.println("主线程~~~~~"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
中断线程【线程中断】
PS:这个方法是Java用来替代stop方法,用来停止线程使用
吐槽:这个方法真感觉“呵呵”,这个方法本身不具备任何停止能力,这个人方法只是一个【中断标记】,可以在线程中检查这个【中断标记】,如果让线程检查到了就让线程停止【这个停止其实就是(return或break)】
stop方法是真心好用,但是需要注意如果使用一定要承担风险【莫名其妙崩溃,会影响其他线程停止】
中断线程需要两个方法合作:
interrupt 中断标记,向运行中的线程添加一个【标记】
interrupted 中断标记检测,检测线程中是否有中断标记,如果有可以进行进一步处理,如果没有这放弃【true和false】
失败案例:
package com.qfedu.SetDeamon;
public class InterruptDemo extends Thread {
@Override
public void run() {
for(int i = 1;i<=10;i++){
System.out.println("子线程中i的值是:"+i);
if(i == 5){
interrupt();//在API中的翻译是中断线程
// stop();
}
}
}
public static void main(String[] args) {
Thread thread1 = new InterruptDemo();
thread1.start();
}
}
正确案例:
package com.qfedu.SetDeamon;
public class InterruptDemo2 extends Thread {
@Override
public void run() {
for(int i = 1;i<=10;i++){
System.out.println("子线程中i的值是:"+i);
if(i == 5){
if(Thread.interrupted()){//判断中断标是否存在
System.out.println("线程已经中断");
return;
}
}
}
}
public static void main(String[] args) {
Thread thread1 = new InterruptDemo2();
thread1.start();
thread1.interrupt();//启动线程后开启了中断标记
}
}
中断睡眠
package com.qfedu.SetDeamon;
//中断睡眠
public class InterruptSleepDemo extends Thread {
@Override
public void run() {
System.out.println("进入线程");
try {
Thread.sleep(2000);
System.out.println("线程完成自然睡眠醒来");
} catch (InterruptedException e) {
System.out.println("线程被终止睡眠");
}
}
public static void main(String[] args) {
InterruptSleepDemo isd = new InterruptSleepDemo();
isd.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isd.interrupt();
}
}
同步代码块【同步锁】
需求: 线程进行火车票售卖,一共有且仅有100张,4个窗口同时售卖
PS:.4个窗口相当于是4个线程对象,一共买100张票,这个票不允许出现负数,不允许为0
多线程并发访问同一个资源的时候,建议将这个资源的设置为static修饰【静态所有对象共享】
package com.qfedu.Synchronized;
//保证100张票
public class SellTicket1 extends Thread{
//将当前多线程并发访问变量设置为静态,就可以保证全局唯一【多有对象共享静态变量】
private static int tickets = 100;
@Override
public void run() {
//执行买票的逻辑
for(int i = 0;i<100;i++){ //执行的卖票的次数
if(tickets > 0){//证明票是够的,买票
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前售票员:【"+Thread.currentThread().getName()+"】"+"第【"+(tickets--)+"】票");
}
}
}
public static void main(String[] args) {
//这里将当票写成员变量,成员变量的特点,就是每个对象都维护自己的成员
//只想卖出100张票【需要使用的同一个对象】
// SellTicket1 st1 = new SellTicket1();
// Thread t1 = new Thread(st1,"刘德华");
// Thread t2 = new Thread(st1,"张学友");
// Thread t3 = new Thread(st1,"郭富城");
// Thread t4 = new Thread(st1,"吴奇隆");
//400张票 将票定义成成员变量
// Thread t1 = new SellTicket1();
// Thread t2 = new SellTicket1();
// Thread t3 = new SellTicket1();
// Thread t4 = new SellTicket1();
// t1.setName("刘德华");
// t2.setName("张学友");
// t3.setName("郭富城");
// t4.setName("吴奇隆");
Thread t1 = new Thread(new SellTicket1(),"刘德华");
Thread t2 = new Thread(new SellTicket1(),"张学友");
Thread t3 = new Thread(new SellTicket1(),"郭富城");
Thread t4 = new Thread(new SellTicket1(),"吴奇隆");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
问题:
1.所有对象共享一个票资源,无论是Thread还是当前Runnable都能保证是同一个,建议将当前资源修改为static,保证所有对象共享其一
2当前线程执行过快,为了解决这个在当前run方法中,添加了Sleep方法,但是添加完方法后,出现重票,出现0,但是可能还会负票
在多线程并发访问同一个临界资源【票】,如果能保证临界资源安全【即正确计算】
Java中提供了一个策略就是对操作临界资源位置,添加【锁对象】,保证当前只能有一个线程操作临界资源,这样就可以保证临界资源安全
同步代码块和同步方法
PS:同步代码块也可以叫做【同步锁,对象锁,同步代码锁】
同步代码块:
保证在多线程并发访问的前提下,只有一个线程可以操作临界资源,这个线程不执行完毕,其他线程无法执行
PS:在同一个时间段内,只能有一个线程对象持有锁资源【锁对象】,当前线程不释放锁资源之前,其他线程没有能力触发这个临界资源
为了提高代码的效率,尽量减少同步代码块的范围,即什么位置操作临界资源,就在什么位置使用
同步代码块语法
synchronized(传一个唯一的对象){
包含操作临界资源的代码
}
*特别说明:*
1.synchronized后面传入的这个对象,我们就成为同步代码块的"锁对象即锁资源"
PS:很多地方法讲解同步代码块的时候,都习惯性使用"this"作为当前做锁对象,不要使用这个,因为"this"会出现锁不住
private static final Object obj = new Object(); //自己创建一个锁对象
直接使用字符串 "" 空字符串的形式代表锁对象, 因为字符串是不可改变
2.每个线程执行到同步代码块的时候都会持有当前锁资源对象,在线程没有执行完毕之前,这个锁对象是不会释放的,多个线程在同一个时间段内只有一个线程持有这个锁对象,以保证临界资源安全【只有一个线程操作】
3.sleep方法在同步代码块中是不会被释放CPU资源
package com.qfedu.Synchronized;
//保证100张票
public class SellTicket1 extends Thread{
//将当前多线程并发访问变量设置为静态,就可以保证全局唯一【多有对象共享静态变量】
private static int tickets = 100;
@Override
public void run() {
//执行买票的逻辑
for(int i = 0;i<100;i++){ //执行的卖票的次数
synchronized ("") { //只要添加一个同步代码块即可,保证锁资源唯一
if (tickets > 0) {//证明票是够的,买票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前售票员:【" + Thread.currentThread().getName() + "】" + "第【" + (tickets--) + "】票");
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new SellTicket1(),"刘德华");
Thread t2 = new Thread(new SellTicket1(),"张学友");
Thread t3 = new Thread(new SellTicket1(),"郭富城");
Thread t4 = new Thread(new SellTicket1(),"吴奇隆");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步方法
同步方法其实就是同步代码块的修改版本【同步方的的锁对象是"this“并且不可以修改】,开发中建议使用同步代码块,不建议使用同步方法
访问权限修饰符 synchronized 返回值类型 方法名(形参列表){
临界资源的操作
}
package com.qfedu.Synchronized;
//保证100张票
public class SellTicket1 extends Thread{
//将当前多线程并发访问变量设置为静态,就可以保证全局唯一【多有对象共享静态变量】
private static int tickets = 100;
@Override
public void run() {
//执行买票的逻辑
for(int i = 0;i<100;i++){ //执行的卖票的次数
seller();
}
}
//同步方法,此时默认的锁就是this
public synchronized void seller() {
if (tickets > 0) {//证明票是够的,买票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前售票员:【" + Thread.currentThread().getName() + "】" + "第【" + (tickets--) + "】票");
}
}
public static void main(String[] args) {
SellTicket1 st1 = new SellTicket1();
Thread t1 = new Thread(st1,"刘德华");
Thread t2 = new Thread(st1,"张学友");
Thread t3 = new Thread(st1,"郭富城");
Thread t4 = new Thread(st1,"吴奇隆");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
ps:必须保证当前类的对象是惟一的
静态同步代码块和静态同步方法
PS:静态同步代码块和静态同步方法和同步代码块和同步方法有所区别,区别在于锁对象
静态同步代码块使用锁对象是一个字节码文件对象【类名.class】
静态同步方法,当前类是锁对象
静态同步方法和同步代码块也被称之为【类锁】
语法:
synchronized(类锁){ //类名.class
临界资源
}
package com.qfedu.Synchronized;
//保证100张票
public class SellTicket1 extends Thread{
//将当前多线程并发访问变量设置为静态,就可以保证全局唯一【多有对象共享静态变量】
private static int tickets = 20;
@Override
public void run() {
//执行买票的逻辑
for(int i = 0;i<20;i++){ //执行的卖票的次数
//静态同步代码块,字节码文件对象
synchronized (String.class){
if (tickets > 0) {//证明票是够的,买票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前售票员:【" + Thread.currentThread().getName() + "】" + "第【" + (tickets--) + "】票");
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new SellTicket1(),"刘德华");
Thread t2 = new Thread(new SellTicket1(),"张学友");
Thread t3 = new Thread(new SellTicket1(),"郭富城");
Thread t4 = new Thread(new SellTicket1(),"吴奇隆");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
访问权限修饰符 static synchronized 返回值类型 方法名(参数列表){
临界资源
}
package com.qfedu.Synchronized;
//保证100张票
public class SellTicket1 extends Thread{
//将当前多线程并发访问变量设置为静态,就可以保证全局唯一【多有对象共享静态变量】
private static int tickets = 100;
@Override
public void run() {
//执行买票的逻辑
for(int i = 0;i<100;i++){ //执行的卖票的次数
seller();
}
}
//静态同步方法,此时默认的锁就是当前类的字节码对象,即 SellTickets1.class
public static synchronized void seller() {
if (tickets > 0) {//证明票是够的,买票
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前售票员:【" + Thread.currentThread().getName() + "】" + "第【" + (tickets--) + "】票");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new SellTicket1(),"刘德华");
Thread t2 = new Thread(new SellTicket1(),"张学友");
Thread t3 = new Thread(new SellTicket1(),"郭富城");
Thread t4 = new Thread(new SellTicket1(),"吴奇隆");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
如何选中那种操作?
类锁【静态同步代码块和静态同步方法】在开发中相对使用较少,静态同步方法使用还是比较多,建议同步代码块使用对象锁。
synchronized的好与坏
好处:保证了多线程并发访问是同步操作安全
缺点:使用synchronized的方法或代码块性能稍低,所以尽量减少使用范围
PS:Java5中提供一个替代synchronized的锁对象 【Lock】