文章目录
- 23.01 多线程程序的引入
- 23.02 进程概述及多进程的意义
- 23.03 线程概述及多线程的意义
- 23.04 并行和并发的区别
- 23.05 Java程序运行原理和JVM的启动是多线程的吗
- 23.06 实现多线程及多线程方式1的思路
- 23.07 多线程方式1的代码实现
- 23.08 获取和设置线程对象名称
- 23.09 线程调度及获取和设置线程优先级
- 23.10 线程控制之休眠线程
- 23.11 线程控制之加入线程
- 23.12 线程控制之礼让线程
- 23.13 线程控制之守护线程
- 23.14 线程控制之中断线程
- 23.15 线程生命周期图解
- 23.16 多线程方式2的思路及代码实现
- 23.17 多线程两种方式的图解比较及区别
- 23.18 继承Thread类的方式卖电影票案例
- 23.19 实现Runnable接口的方式卖电影票案例
- 23.20 买电影票出现了同票和负数票的原因分析
- 23.21 线程安全问题的产生原因分析
- 23.22 同步代码块的方式解决线程安全问题
- 23.23 同步代码块解决线程安全问题的解释
- 23.24 同步的特点及好处和弊端
- 23.25 同步代码块的锁及同步方法应用和锁的问题
- 23.26 以前的线程安全的类回顾
- 23.27 总结
23.01 多线程程序的引入
如果一个程序只有一个执行流程,所以这样的程序就是单线程程序。
如果一个程序有多条执行流程,那么,该程序就是多线程程序。
23.02 进程概述及多进程的意义
要想说线程,首先必须得知道进程,因为线程是依赖于进程存在的
进程:正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
多进程意义:多进程的作用不是提高执行速度,而是提高CPU的使用率
单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。
比如:Windows,Mac和Linux等,能在同一个时间段内执行多个任务。
对于单核计算机来讲,游戏进程和音乐进程不是同时运行的,因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。
23.03 线程概述及多线程的意义
在一个进程内部又可以执行多个任务,而这每一个任务就可以看成是一个线程。线程是程序中单个顺序的控制流,是程序使用CPU的基本单位。
线程:
是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
多线程意义: 多线程的作用也不是提高执行速度,而是为了提高应用程序的使用率。
多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈。所以他们仍然是在抢CPU的资源执行。一个时间点上只有能有一个线程执行。而且谁抢到,这个不一定,所以,造成了线程运行的随机性。
23.04 并行和并发的区别
注意两个词汇的区别:并行和并发
并行是逻辑上同时发生,指在某一个时间内同时运行多个程序
并发是物理上同时发生,指在某一个时间点同时运行多个程序
23.05 Java程序运行原理和JVM的启动是多线程的吗
Java程序运行原理:java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
由于JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的
23.06 实现多线程及多线程方式1的思路
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,但是Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。
Java提供的类 Thread
通过查看API,知道有2中方式实现多线程程序。
方式1:继承Thread类
步骤:
1:自定义类MyThread继承Thread类。
2:MyThread类重写run()方法
3:创建对象
4:启动线程
23.07 多线程方式1的代码实现
package com.example_02;
/*
* 需求:我们要实现多线程的程序。
* 如何实现呢?
* 由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
* 而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
* Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
* 但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
* 由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
* 然后提供一些类供我们使用。我们就可以实现多线程程序了。
* 那么Java提供的类是什么呢?
* Thread
* 通过查看API,我们知道了有2中方式实现多线程程序。
*
* 方式1:继承Thread类。
* 步骤
* A:自定义类MyThread继承Thread类。
* B:MyThread类里面重写run()?
* 为什么是run()方法呢?
* C:创建对象
* D:启动线程
*/
public class MyThreadDemo {
public static void main(String[] args) {
/* // 创建线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.run();
myThread.run();*/
// 调用run()方法为什么是单线程的呢?
// 因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
// 要想看到多线程的效果,就必须说说另一个方法:start()
// 面试题:run()和start()的区别?
// run():仅仅是封装被线程执行的代码,直接调用是普通方法
// start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
/* MyThread myThread = new MyThread();
myThread.start();
// IllegalThreadStateException:非法的线程状态异常
// 为什么呢?因为这个相当于是my线程被调用了两次。而不是两个线程启动。
myThread.start();*/
//正确做法 创建两个线程对象
MyThread myThread = new MyThread();
MyThread myThread1 = new MyThread();
myThread.start();
myThread1.start();
}
}
/* A:自定义类MyThread继承Thread类。
* 该类要重写run()方法,为什么呢?
* 不是类中的所有代码都需要被线程执行的。
* 而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
*/
class MyThread extends Thread {
@Override
public void run() {
// 一般来说,被线程执行的代码肯定是比较耗时的。所以用循环改进
for (int i = 0; i < 300; i++) {
System.out.println(i);
}
}
}
run()和start()的区别
run():仅仅是封装被线程执行的代码,直接调用是普通方法
start():首先启动了线程,然后再由jvm去调用该线程的run()方法
继承Thread类的类为什么要重写run()方法?
不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
23.08 获取和设置线程对象名称
public final String getName():返回该线程的名称
public final void setName(String name):改变线程名称,使之与参数 name 相同。
package com.example_03;
/*
* 如何获取线程对象的名称呢?
* public final String getName():获取线程的名称。
* 如何设置线程对象的名称呢?
* public final void setName(String name):设置线程的名称
*
* 针对不是Thread类的子类中如何获取线程对象名称呢?
* public static Thread currentThread():返回当前正在执行的线程对象
* Thread.currentThread().getName()
*/
public class MyThreadDemo {
public static void main(String[] args) {
/*
//创建线程对象
//无参构造+setXxx()
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//调用方法设置名称
my1.setName("张三");
my2.setName("李四");
my1.start();
my2.start();
*/
//带参构造方法给线程起名字
MyThread my1 = new MyThread("张三");
MyThread my2 = new MyThread("李四");
my1.start();
my2.start();
//我要获取main方法所在的线程对象的名称,该怎么办呢?
//遇到这种情况,Thread类提供了一个很好玩的方法:
//public static Thread currentThread():返回当前正在执行的线程对象
System.out.println(Thread.currentThread().getName());
}
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println(getName() + ":" + i);
}
}
}
运行结果
main
张三:0
张三:1
张三:14
张三:15
李四:0
通过Thread类的getName( )方法可以获取线程的名称,默认的命名方式是:Thread-编号(从0开始)
为什么名称是:Thread-编号?
看源码:
名称为什么是:Thread-? 编号
class Thread {
private char name[];
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
//大部分代码被省略了
this.name = name.toCharArray();
}
public final void setName(String name) {
this.name = name.toCharArray();
}
private static int threadInitNumber; //0,1,2
private static synchronized int nextThreadNum() {
return threadInitNumber++; //return 0,1
}
public final String getName() {
return String.valueOf(name);
}
}
class MyThread extends Thread {
public MyThread() {
super();
}
}
23.09 线程调度及获取和设置线程优先级
线程调度:
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
设置和获取线程优先级
public final int getPriority():返回线程的优先级。
public final void setPriority(int newPriority):更改线程的优先级。
package com.example_04;
/*
* 我们的线程没有设置优先级,肯定有默认优先级。
* 那么,默认优先级是多少呢?
* 如何获取线程对象的优先级?
* public final int getPriority():返回线程对象的优先级
* 如何设置线程对象的优先级呢?
* public final void setPriority(int newPriority):更改线程的优先级。
*
* 注意:
* 线程默认优先级是5。
* 线程优先级的范围是:1-10。
* 线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
*
* IllegalArgumentException:非法参数异常。
* 抛出的异常表明向方法传递了一个不合法或不正确的参数。
*
*/
public class ThreadPriorityDemo {
public static void main(String[] args) {
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("张三");
tp2.setName("李四");
tp3.setName("王五");
//线程默认优先级是5。
System.out.println(tp1.getPriority()); //5
System.out.println(tp2.getPriority()); //5
System.out.println(tp3.getPriority()); //5
// 设置线程优先级
//tp1.setPriority(100000);
//设置正确的线程优先级
tp1.setPriority(10);
tp2.setPriority(1);
tp1.start();
tp2.start();
tp3.start();
}
}
class ThreadPriority extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(getName() + ":" + i);
}
}
}
23.10 线程控制之休眠线程
public static void sleep(long millis)throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。
例:
package com.example_04;
import java.util.Date;
/*
* 线程休眠
* public static void sleep(long millis)
*/
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ThreadSleep ts3 = new ThreadSleep();
ts1.setName("林青霞");
ts2.setName("林志玲");
ts3.setName("林志颖");
ts1.start();
ts2.start();
ts3.start();
}
}
class ThreadSleep extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + ":" + i + ",日期:" + new Date());
// 睡眠
// 困了,我稍微休息1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
23.11 线程控制之加入线程
public final void join()throws InterruptedException
等待该线程终止。
package com.example_04;
/*
* public final void join():等待该线程终止。
*/
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("李渊");
tj2.setName("李世民");
tj3.setName("李元霸");
tj1.start();
try {
//等待线程李渊线程,线程李渊运行完成后,线程李世民线程李元霸开始争夺资源
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
class ThreadJoin extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(getName() + ":" + i);
}
}
}
23.12 线程控制之礼让线程
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
package com.example_04;
/*
* public static void yield():暂停当前正在执行的线程对象,并执行其他线程。
* 让多个线程的执行更和谐,但是不能靠它保证一人一次。
*/
public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();
ty1.setName("张三");
ty2.setName("李四");
ty1.start();
ty2.start();
}
}
class ThreadYield extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.println(getName() + ":" + i);
//暂停当前线程,并执行其他线程,不能保证机会均等
Thread.yield();
}
}
}
23.13 线程控制之守护线程
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
package com.example_04;
/*
* public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
* 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
*
* 游戏:坦克大战。
*/
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("关羽");
td2.setName("张飞");
//标记为守护线程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
Thread.currentThread().setName("刘备");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class ThreadDaemon extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + ":" + i);
}
}
}
23.14 线程控制之中断线程
public final void stop(): 让线程停止,过时了,但是还可以使用。
public void interrupt(): 中断线程。 把线程的状态终止,并抛出一个InterruptedException。
package com.example_04;
import java.util.Date;
/*
* public final void stop():让线程停止,过时了,但是还可以使用。
* public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。
*/
public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();
// 你超过三秒不醒过来,我就干死你
try {
Thread.sleep(3000);
// ts.stop(); //使用stop()后面的语句都不会执行
ts.interrupt(); //后面的语句会执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());
// 我要休息10秒钟,亲,不要打扰我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}
System.out.println("结束执行:" + new Date());
}
}
23.15 线程生命周期图解
23.16 多线程方式2的思路及代码实现
步骤:
1:自定义类MyRunnable实现Runnable接口
2:重写run()方法
3:创建MyRunnable类的对象
4:创建Thread类的对象,并把3步骤的对象作为构造参数传递
package com.example_05;
/*
* 方式2:实现Runnable接口
* 步骤:
* A:自定义类MyRunnable实现Runnable接口
* B:重写run()方法
* C:创建MyRunnable类的对象
* D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
*/
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,并把C步骤的对象作为构造参数传递
//Thread(Runnable target)
/* Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("张三");
t2.setName("李四");*/
//public Thread(Runnable target,String name):分配新的 Thread 对象。
Thread t1 = new Thread(my,"张三");
Thread t2 = new Thread(my,"李四");
t1.start();
t2.start();
}
}
//A:自定义类MyRunnable实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
23.17 多线程两种方式的图解比较及区别
实现接口方式的好处
1.可以避免由于Java单继承带来的局限性。
2.适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
23.18 继承Thread类的方式卖电影票案例
某电影院电影票共有100张,有3个售票窗口售票,请设计一个程序模拟该电影院售票。
package com.example_06;
/*
* 某电影院目前正在上映贺岁大片(红高粱,少林寺传奇藏经阁),共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
* 继承Thread类来实现。
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 创建三个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();
// 给线程对象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
// 启动线程
st1.start();
st2.start();
st3.start();
}
}
class SellTicket extends Thread {
// 定义100张票
// private int tickets = 100;
// 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
private static int tickets = 100;
@Override
public void run() {
// 定义100张票
// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
// int tickets = 100;
// 是为了模拟一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
23.19 实现Runnable接口的方式卖电影票案例
package com.example_07;
/*
* 实现Runnable接口的方式实现
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
}
23.20 买电影票出现了同票和负数票的原因分析
相同的票出现多次:CPU的一次操作必须是原子性的
出现了负数的票:随机性和延迟导致的
package com.example_08;
/*
* 实现Runnable接口的方式实现
*
* 通过加入延迟后,就产生了连个问题:
* A:相同的票卖了多次
* CPU的一次操作必须是原子性的
* B:出现了负数票
* 随机性和延迟导致的
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
// 理想状态:
// 窗口1正在出售第100张票
// 窗口2正在出售第99张票
// 但是呢?
// CPU的每一次执行必须是一个原子性(最简单基本的)的操作。
// 先记录以前的值
// 接着把ticket--
// 然后输出以前的值(t2来了)
// ticket的值就变成了99
// 窗口1正在出售第100张票
// 窗口2正在出售第100张票
}
}
}
/*
@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 1;
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
//窗口1正在出售第1张票,tickets=0
//窗口2正在出售第0张票,tickets=-1
//窗口3正在出售第-1张票,tickets=-2
}
}
}
*/
}
23.21 线程安全问题的产生原因分析
线程安全问题产生的原因:
1.多个线程在操作共享的数据
2.操作共享数据的线程代码有多条
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生
23.22 同步代码块的方式解决线程安全问题
思想:把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,其他线程不能来执行。Java提供了:同步机制。
同步代码块:
synchronized(对象)
{需要同步的代码;}
package com.example_09;
/*
* 如何解决线程安全问题呢?
*
* 要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准)
* A:是否是多线程环境
* B:是否有共享数据
* C:是否有多条语句操作共享数据
*
* 我们来回想一下我们的程序有没有上面的问题呢?
* A:是否是多线程环境 是
* B:是否有共享数据 是
* C:是否有多条语句操作共享数据 是
*
* 由此可见我们的程序出现问题是正常的,因为它满足出问题的条件。
* 接下来才是我们要想想如何解决问题呢?
* A和B的问题我们改变不了,我们只能想办法去把C改变一下。
* 思想:
* 把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。
* 问题是我们不知道怎么包啊?其实我也不知道,但是Java给我们提供了:同步机制。
*
* 同步代码块:
* synchronized(对象){
* 需要同步的代码;
* }
*
* A:对象是什么呢?
* 我们可以随便创建一个对象试试。
* B:需要同步的代码是哪些呢?
* 把多条语句操作共享数据的代码的部分给包起来
*
* 注意:
* 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
* 多个线程必须是同一把锁。
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
//锁对象必须是同一个
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
}
}
23.23 同步代码块解决线程安全问题的解释
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
// 定义同一把锁
private Object obj = new Object();
@Override
public void run() {
while (true) {
// t1,t2,t3都能走到这里
// 假设t1抢到CPU的执行权,t1就要进来
// 假设t2抢到CPU的执行权,t2就要进来,发现门是关着的,进不去。所以就等着。
// 门(开,关)
synchronized (obj) { // 发现这里的代码将来是会被锁上的,所以t1进来后,就锁了。(关)
if (tickets > 0) {
try {
Thread.sleep(100); // t1就睡眠了
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
//窗口1正在出售第100张票
}
else {
break;
}
} //t1就出来可,然后就开门。(开)
}
}
23.24 同步的特点及好处和弊端
同步的前提:多个线程且多个线程使用的是同一个锁对象
同步的好处:同步的出现解决了多线程的安全问题
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
23.25 同步代码块的锁及同步方法应用和锁的问题
同步方法就是把同步关键字加到方法上
//同步方法
private synchronized void sellTicket()
{
if (tickets > 0)
{
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
1.同步代码块的锁对象是任意对象
2.同步函数使用的锁是this
3.静态的同步函数使用的锁是该函数所属的字节码文件对象可以用getClass方法获取,也可以用当前类名.class表示
23.26 以前的线程安全的类回顾
Collections中让集合同步功能
package com.example_12;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;
/**
* @author chenjianbing
* @create 2021-09-23 11:44
*/
public class ThreadDemo {
public static void main(String[] args) {
// 线程安全的类
StringBuffer sb = new StringBuffer();
Vector<String> v = new Vector<String>();
Hashtable<String, String> h = new Hashtable<String, String>();
// Vector是线程安全的时候才去考虑使用的,但是我还说过即使要安全,我也不用你
// 那么到底用谁呢?
// public static <T> List<T> synchronizedList(List<T> list)
List<String> list1 = new ArrayList<String>();// 线程不安全
List<String> list2 = Collections
.synchronizedList(new ArrayList<String>()); // 线程安全
}
}
23.27 总结
1:多线程(理解)
(1)多线程:一个应用程序有多条执行路径
进程:正在执行的应用程序
线程:进程的执行单元,执行路径
单线程:一个应用程序只有一条执行路径
多线程:一个应用程序有多条执行路径
多进程的意义?
提高CPU的使用率
多线程的意义?
提高应用程序的使用率
(2)Java程序的运行原理及JVM的启动是多线程的吗?
A:Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。
B:JVM的启动是多线程的,因为它最低有两个线程启动了,主线程和垃圾回收线程。
(3)多线程的实现方案(自己补齐步骤及代码 掌握)
A:继承Thread类
1:自定义类MyThread继承Thread类。
2:MyThread类重写run()方法
3:创建对象
4:启动线程
B:实现Runnable接口
1:自定义类MyRunnable实现Runnable接口
2:重写run()方法
3:创建MyRunnable类的对象
4:创建Thread类的对象,并把3步骤的对象作为构造参数传递
(4)线程的调度和优先级问题
A:线程的调度
a:分时调度
b:抢占式调度 (Java采用的是该调度方式)
B:获取和设置线程优先级
a:默认是5
b:范围是1-10
(5)线程的控制(常见方法)
A:休眠线程
B:加入线程
C:礼让线程
D:后台线程
E:终止线程(掌握)
(6)线程的生命周期(参照 线程生命周期图解.bmp)
A:新建
B:就绪
C:运行
D:阻塞
E:死亡
(7)电影院卖票程序的实现
A:继承Thread类
B:实现Runnable接口
(8)电影院卖票程序出问题
A:为了更符合真实的场景,加入了休眠100毫秒。
B:卖票问题
a:同票多次
b:负数票
(9)多线程安全问题的原因(也是我们以后判断一个程序是否有线程安全问题的依据)
A:是否有多线程环境
B:是否有共享数据
C:是否有多条语句操作共享数据
(10)同步解决线程安全问题
A:同步代码块
synchronized(对象) {
需要被同步的代码;
}
这里的锁对象可以是任意对象。
B:同步方法
把同步加在方法上。
这里的锁对象是this
C:静态同步方法
把同步加在方法上。
这里的锁对象是当前类的字节码文件对象(反射再讲字节码文件对象)
(11)回顾以前的线程安全的类
A:StringBuffer
B:Vector
C:Hashtable
D:如何把一个线程不安全的集合类变成一个线程安全的集合类
用Collections工具类的方法即可。