- 同步和异步的区别
同步: 多个线程在同步过程中,只有一个线程在工作,其他线程在等待,这个过程是单线程的(串行)
异步: 多个线程同时在在进行,各干各的事(并行)
8.1.程序,进程,线程的概念
- 程序 静态,相较于动态来说,静指没有加载到内存中,没有CPU没有参与运算,默默存储在存储空间中.
- 进程: 把程序运行起来,这时需要加载到内存空间中,同时需要CPU分配计算资源开始做运算,可可理解为正在运行的一个程序.
- 线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径.
每个线程,拥有自己独立的: 栈,程序计数器
多个线程,共享同一个进程的结构: 方法区,堆
- 单核CPU和多核CPU的理解
单核CPU,其实是一种假的多线程,因为CPU可以快速切换,看起来像并行执行
多核CPU,肯定是多个线程同时执行 - 并行和并发的理解
并行: 多个CPU同时执行多个任务. 比如: 多个人同时做不同的事: 多个篮球场,每个场都在玩
并发: 一个CPU(采用时间片)同时执行多个任务.比如: 秒杀,多个人做同一件事: 某一个场所有人都去抢一个篮球
8.2.线程的创建和使用
- 创建多线程方式一: 继承Thread类
- 创建线程过程中两个问题的说明
多线程的创建,方式一: 继承于Thread类
1.创建一个继承Thread类的子类
2.重写Thread类的run()方法 --> 将此线程要执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()
public class ThreadTest {
public static void main(String[] args) {
// main方法里面是主线程做的事
// 3.创建Thread类子类的对象
MyThread t1 = new MyThread();
// 4.通过上面对象调用thread类的start(): ①启动当前线程 ②启动到线程后自动地start方法会调用当前线程的run()
// 调start之前包括调他本身,都是主(main)线程帮做的事
// 此时有两个线程同时在执行,彼此具有交互性,
t1.start(); // 要想启动线程,必须调start方法,不要去调run()
// 问题一: 不能通过直接调用run()的方式启动线程
// t1.run(); // 没有多分出来一个线程,就只是一个造了对象,调Thread类的run方法,只是体现对象调方法,根本没开启新的线程,这个方法执行完后才会走下面的逻辑,意味着,这个调的run方法里面的逻辑仍然是在主线程当中做的
// 问题二: 再启动一个线程,遍历100以内的偶数,不能还让已经调start()的线程去执行,会报IllegalThreadStateException异常
// t1.start(); // 报错,一个线程只能start一次
// 要想创建多个线程就去造多个对象
// 需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
// 如下操作仍然是main线程中执行的
for (int i = 0;i < 100;i++){
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i + " main()");
}
}
}
}
// 1.创建一个继承于Thread类的子类
class MyThread extends Thread{
// 2.重写Thread类的run方法
@Override
public void run() {
for (int i = 0;i < 100;i++){
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
- 继承方式的练习:
创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历线程100以内的奇数
public class ThreadDemo {
public static void main(String[] args) {
// 创建两个子类的对象
MyThread1 m1 = new MyThread1();
MyThread2 m2 = new MyThread2();
// 两个对象分别调Thread类的start方法启动线程
m1.start();
m2.start();
// 创建Thread类的匿名子类的的匿名对象方式(简便写法)
// new Thread().start(); // 这样写不对,这里调start是调Thread自己类里的run了,要调的是新线程重写的run方法
new Thread(){ // 重写了run方法后这里new的就是Thread类的匿名子类的对象,子类没名,就用Thread类来充当
@Override
public void run() {
for (int i = 0; i < 100; i++){
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
// 两个线程做的事不一样,造两个Thread类的子类
// 遍历偶数的子类
class MyThread1 extends Thread{
// 重写Thread类的run方法
@Override
public void run() {
for (int i = 0; i < 100; i++){
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
// 遍历奇数的子类
class MyThread2 extends Thread{
// 重写Thread类的run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
- 线程的常用方法:
测试Thread类的常用方法:
1.start():启动当前线程; 调用当前线程的run()
2.run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread(): Thread类的静态方法,返回执行当前代码的线程的对象,方法定义就是返回一个当前实例
相当于线程的name属性的set,get方法
4.getName(): 获取当前线程的名字
5.setName(): 设置当前线程的名字
6.yield(): 释放当前CPU的使用权.当然有可能在下一刻有分配到当前线程
7.join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,CPU想让他执行也执行不了,直到线程b完全执行完以后,线程a才结束阻塞状态,接下来就看CPU啥时候给你分配资源了,分配到资源就接着往后执行
8.stop(): 已过时,当执行此方法时,强制结束当前线程.该线程就进入生命周期末尾了,直接就消亡了
9.sleep(long millitime): 让当前线程"睡眠"指定的millitime毫秒,睡眠完,等着CPU给分配资源,分配到就可以执行.在指定的millitime毫秒时间内,当前线程是阻塞状态
10.isAlive(): 判断当前线程是否存活
线程通信: wait()/ notify()/notifyAlll(): 此三个方法定义在Object类中的
- 补充 : 线程的分类
一种是守护线程,例如gc()垃圾回收线程
一种是用户线程,例如: main()主线程,用户线程结束,守护线程也结束
- 线程的调度
- 线程优先级设置
线程的优先级:
- 分为10档
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5 --> 默认线程优先级- 如何获取和设置当前线程的优先级:
getPriority(): 获取线程的优先级
setPriority(int p): 设置线程的优先级
说明: 高优先级的线程要抢占低优先级线程CPU的执行权.但只是从概率上讲,高优先级的线程会高概率的情况下被执行.
并不意味着只有当高优先级的线程执行完后,才执行低优先级的线程
public class ThreadMethodTest {
public static void main(String[] args) {
// 默认提供的线程名为Thread-0,因为这里调的是当前子类的空参构造器,该构造器就会调父类的super(),根据Thread源码,Thread类的空参构造器初始线程名为Thread-0,依次递增
// 给线程命名方式二: 通过构造器
HelloThread h1 = new HelloThread("Thread:1");
// 自定义线程名方式一:
// h1.setName("线程一"); // 必须在start()前执行,如果在线程启动后就晚了
// 设置分线程的优先级,同样在线程启动前设置
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
// 给主线程命名: 在执行getName()前执行才有效
Thread.currentThread().setName("主线程");
// 设置主线程优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
// 输出当前线程的线程名,线程优先级
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
/*if (i == 20) {
// 要线程去调join(),因为join是Thread类里的方法,而在main方法所在是public类,这个类没有join方法,所以直接调报错
// 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
try {
h1.join(); // 到i==20当分线程执行完后才开始主线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
}
// System.out.println(h1.isAlive()); // 判断h1线程是否存活,线程还没执行完就是还活着
}
}
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
// sleep();抛的异常只能用try-catch解决不能用throws,因为该类的run方法是重写Thread类的run方法,而Thread类的run方法没有抛异常
// 父类的方法没有throws过,子类重写的方法就一定不能throws; 子类抛的异常≤父类的异常,所以sleep()只能用try-catch
// sleep的意思是: 一旦线程执行到sleep方法的时候,他就也阻塞了一秒,一秒结束了也不是马上输出后面的信息,还要等CPU分配给出资源,得到资源了才能继续往后走
// 一秒钟内,即使CPU想分配资源也不能往下走,因为此时线程处于阻塞状态
/*try {
sleep(10); // 让当前线程强制阻塞,
}catch(InterruptedException e){
e.printStackTrace();
}*/
// 输出当前线程的线程名,线程优先级
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
/*if (i % 20 == 0) {
// 当前线程就是当前类的对象
// yield(); 释放CPU执行权,但也有可能还是原来的线程抢夺回CPU的使用权
this.yield(); // this就是当前类的对象,相当于h1,也就是当前线程Thread.currentThread(),this省略掉也可以呀
}*/
}
}
// 在子类中通过构造器给线程命名
public HelloThread(String name) {
super(name); // 调用Thread类的带参构造器
}
}
- 例题: 继承Thread方式: 多窗口卖票
例子: 创建三个窗口卖票,总票数为100张
存在线程的安全问题,待解决
public class WindowTest {
public static void main(String[] args) {
// 创建三个线程对象
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window extends Thread {
private static int ticket = 100; // 静态属性,造几个线程总共就只有一百张票,每个对象共享同一个静态变量
public Window(){
}
// 重写Thread类的run方法
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + " 卖票,票号为: " + ticket);
ticket--;
}else{
break;
}
}
}
}
- 创建多线程方式二: 实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法: run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
// 1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2.实现类去实现Runnable中的抽象方法: run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i); // 这里不能直接用getName(),因为MThread类没有继承Thread而是继承了Object类,实现的接口也没有getName方法
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
// 3.创建实现类的对象
MThread mthread = new MThread();
// 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mthread);// 多态形式: 实参: Runnable target = new mThread();
// 5.通过Thread类对象调用start(),①启动线程;②调用当前线程的run() --> 调用了Runnable类型的target的run(),target被mthread赋值,所以:
// 根据Thread类的源码,因为Thread类的其中一个构造器有Runnable target形参,而此形参是Thread中定义的实现的Runnable接口为类型的变量
// 根据Thread类重写的run()源码得知,如果形参target有被赋值,则调用该形参的run方法,没赋值则调用继承Thread子类重写的run(),所以就调mthread的run()
t1.setName("线程一");
t1.start();// 这时线程是t1,谁start线程就谁
// 再启动一个线程,遍历100以内偶数,共用同一个接口实现类就行
Thread t2 = new Thread(mthread); // 匿名类
t2.setName("线程二");
t2.start(); // 最后也会回归到所在接口实现类的run方法的调用
}
}
- 例题: 实现Runnable方式: 多窗口卖票
class Window1 implements Runnable{
// 因为只创建了一个Window1类的对象,所以ticktet是同一个,所以不用static
private int ticket = 100;
// 重写run()
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
// 三个线程构造器共用同一个new Window1(),一个窗口三个线程
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
- 两种创建方式的对比:
比较创建线程的两种方式:
开发中: 优先选择: 实现Runnable接口的方式
原因:
1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合来处理多个线程有共享数据的情况.可以把多个线程共享的数据封装在实现Runnable接口的类中,然后这个类的对象就可以作为参数传递到线程(Thread类)的构造器中,天然就是共享数据
联系: public class Thread implements Runnable
相同点: 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中.Thread也是实现Runnable中的run()
8.3.线程的生命周期
image.png8.4.线程同步
- 理解线程的安全问题
- 安全问题的举例和解决措施
例子: 创建三个窗口卖票,总票为100张,用实现Runnable接口的方式
1.问题: 卖票过程中,出现了重票,错票 --> 出现了线程的安全问题
2.问题出现的原因: 当某个线程操作车票的过程中,尚未操作完成出去时,其他线程也参与进来,也操作车票(相当于共享数据).
例如: 去厕所时候,坑位有限,坑位相当于共享数据,每个人都是一个线程,正常来讲,进去了完事后出去,别人再进来,这就是安全的,安全问题: 进去了还没出来,另一个人也进来了,就出现线程安全问题了
3.如何解决: 当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket时,其他线程才可以开始操作ticket.
这种情况即使线程a出现了阻塞,也不能被改变
4.在Java中,通过同步机制,来解决线程的安全问题
方式一: 同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明: 1.操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了,最低使用原则,否则无谓增加线程开销
包少了: 线程在执行剩余的同步代码可能会阻塞,这是没有锁,其他线程就会进来,会混乱
包多了: 假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
2.共享数据: 多个线程共同操作的变量. 比如: ticket就是共享数据
3.同步监视器,俗称: 锁.任何一个类的对象,都可以充当锁(本类对象会栈溢出) 进厕所的时候,这个时候就放一把锁,谁进去就拿着这把锁,没进去的人就拿不到这把锁,谁能拿到锁谁就操作这段代码
同步监视器要求: 多个线程必须要共用同一把锁
补充: 再实现Runnable接口创建多线程的方式中,我们可以考虑用this(还要看是否同一个对象)充当同步监视器
方式二: 同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明为同步的.
5.同步的方式:
好处: 解决了线程的安全问题
局限性:操作同步代码,只能有一个线程参与,其他线程等待.相当于是一个单线程的过程(外面还是并行),效率低
class Window1 implements Runnable{
// 因为只创建了一个Window1类的对象,所以ticktet是同一个,所以不用static
private int ticket = 100; // 共享数据
// 创建随便一个类的对象
//Object obj = new Object();//保证锁的唯一性,要声明在这里,如果声明在run方法里,几个线程就会造几个锁,或者在同步监视器里new对象也不行
Dog dog = new Dog();
// 重写run()
@Override
public void run() { // 多个线程来操作的方法都在run里
while (true){
synchronized (dog) { // 必须共用同一个锁
// 此时能保证包住的这段代码,一个进程进来,即使线程sleep阻塞了,别的线程也得在外等着,直到在执行的线程醒来操作完出去了,其他线程包括本身(抢完还能再抢),看谁又进去了
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
// 三个线程构造器共用同一个new Window1(),一个窗口三个线程
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
// 造一个锁类也可以
class Dog{
}
- 同步代码块处理实现Runnable的线程安全问题
4.在Java中,通过同步机制,来解决线程的安全问题
方式一: 同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明: 1.操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了,最低使用原则,否则无谓增加线程开销
包少了: 线程在执行剩余的同步代码可能会阻塞,这是没有锁,其他线程就会进来,会混乱
包多了: 假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
2.共享数据: 多个线程共同操作的变量. 比如: ticket就是共享数据
3.同步监视器,俗称: 锁.任何一个类的对象,都可以充当锁(本类对象会栈溢出) 进厕所的时候,这个时候就放一把锁,谁进去就拿着这把锁,没进去的人就拿不到这把锁,谁能拿到锁谁就操作这段代码
同步监视器要求: 多个线程必须要共用同一把锁
补充: 再实现Runnable接口创建多线程的方式中,我们可以考虑用this(还要看是否同一个对象)充当同步监视器
class Window1 implements Runnable{
// 因为只创建了一个Window1类的对象,所以ticket是同一个,所以不用static
private int ticket = 100; // 是天然的共享数据
// 创建随便一个类的对象
//Object obj = new Object();//保证锁的唯一性,要声明在这里,如果声明在run方法里,几个线程就会造几个锁,或者在同步监视器里new对象也不行
// Dog dog = new Dog(); // 天然也是共享的锁,因为就只造了一个Window1对象
// 重写run()
@Override
public void run() { // 多个线程来操作的方法都在run里
while (true){ // 循环不能包在同步代码块中,假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
// 用this(当前对象)做同步锁,需要只new一个Window1对象,所以只能用于implements不能用于继承
// this就是w变量,后面可以看到是造的w对象,这里相当于是动态获取的,就是调这个方法的对象就是this,这个方法是在Window1中定义的,Window1的对象就是this.自始至终就只造了一个Window1对象,所以这个this的w就是唯一的
synchronized (this){ //此时的this: 唯一的Window1的对象,用当前对象充当 // 方式二: synchronized (dog) { // 必须共用同一个锁
// 此时能保证包住的这段代码,一个进程进来,即使线程sleep阻塞了,别的线程也得在外等着,直到在执行的线程醒来操作完出去了,其他线程包括本身(抢完还能再抢),看谁又进去了
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
// 三个线程构造器共用同一个new Window1(),一个窗口三个线程
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
// 造一个锁类也可以
class Dog{
}
- 同步代码块处理继承Thread类的线程安全问题
用同步代码块解决继承Thread类的方式的线程安全问题
例子: 创建三个窗口卖票,用继承Thread类的方式
说明: 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑用当前类(类名.class)充当同步监视器,一定保证对象的唯一性
反射就是当类加载进内存后,动态的获取类中的信息,以及用对象调用方法
class Window2 extends Thread{
// 有共享数据
private static int ticket = 100;
// private Object obj = new Object(); // 错误,这时候的锁不唯一,下面new了三个Window2的对象,每个对象都有一个实例变量,每个线程有一个obj,锁就不是共享的
// private static Object obj = new Object(); // 这样写三个Window2的对象才能共享一个obj
// 重写Thread类中的run方法
@Override
public void run() {
while (true) {
// 这是后的this是当前Window2类的对象,当前new了三个对象,不唯一,所以这里不能用this
// synchronized (this) { // 错误方式: this代表t1,t2,t3三个对象
// synchronized (obj) { // 正确的
// 拿当前类充当对象,类也是对象: Class c = Window2.class,类类型 变量 = 变量值(相当于类类型的对象)
// Window2类只加载一次,所以Window2.class对象唯一
synchronized (Window2.class){
if (ticket > 0) {
// 出现错票概率增大
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖票,票号为: " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
同步方法处理线程安全问题
1.同步方法仍然涉及到同步监视器,只是不需要显式的声明,相当于用默认的
2.非静态的同步(synchronized)方法,同步监视器是: this
静态的同步(synchronized)方法,同步监视器是: 当前类本身
- 同步方法处理实现Runnable的线程安全问题
/**
* 用同步方法解决实现Runnable接口的线程安全问题
*/
class Window3 implements Runnable{
private int ticket = 100;
boolean isFlag = true;
// 重写Runnable接口的run方法
@Override
public void run() {
while (isFlag){
show();
}
}
// 同步方法: 在声明方法时加synchronized关键字就行
// 同步方法可以保证在方法内部这些代码和同步代码块包起来一样,方法外面有多个线程,方法里只有一个线程,所以是安全的
// 有默认同步锁: this,因为this是唯一的w对象
public synchronized void show(){ // 同步监视器: this(调用show方法的当前对象)
if (ticket > 0){
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为: " + ticket);
ticket--;
}else{
isFlag = false;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 同步方法处理继承Thread类的线程安全问题
class Window4 extends Thread {
// 有共享数据
private static int ticket = 100;
// 重写Thread类中的run方法
@Override
public void run() {
while (true) {
show();
}
}
// private synchronized void show(){// 同步监视器: t1,t2,t3三个对象,这种方法错误
//类方法随着类加载只加载一次,属于共享的
private static synchronized void show(){ // 同步监视器: Window4.class(当前的类),当前的类唯一,所以安全
if (ticket > 0) {
// 出现错票概率增大
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖票,票号为: " + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
- 线程安全的单例模式之懒汉式
用同步机制将单例模式中的懒汉式改写为线程安全的
双重检查单例模式,属性还要记得加一个volatile关键字
public class BankTest {
public static void main(String[] args) {
}
}
// 懒汉式
class Bank extends Thread{
private Bank(){
}
private static Bank instance = null;
// 有可能线程进入getInstance方法后进入阻塞状态,阻塞后又回到就绪状态,在判断变量后还没赋值的时候,其他线程也进来了
// 简单处理,直接在方法层面上加synchronized,此时就已经是线程安全了,当多个线程调用该方法时候,同步锁是Bank.class(当前类本身),类本身也充当一个对象,锁一定是对象
// public static synchronized Bank getInstance(){
public static Bank getInstance(){
// 方式一: 效率稍差
//里面代码都算是对共享数据的操作
/*synchronized (Bank.class){
if (instance == null){ // 判断变量
instance = new Bank();// 给变量赋值
}
return instance;
}*/
// 方式二: 效率更高 (面试建议写方式二)
// 假设第一批可能有好几个线程一起过来,getInstance方法都能进来,第一个if判断也都能过,都同时排在锁前看谁能抢到,
// 假设线程一抢到了,并且new好对象出去了,后面几个线程要稍微等一下才能进,因为进去发现instance对象不是null了,就直接拿着现成的instance出去了,后面几个似乎是多等了一下
// 但是再后面来的线程,再进来方法的时候,判断第一个if,instance就不是null了,后面来的线程就没必要等着再进入同步代码块了,就直接拿着造好的对象出去,所以比方式一效率稍微高些
if (instance == null){
synchronized (Bank.class){
// 里面两行代码是在操作共享数据
if (instance == null){
instance = new Bank();
}
}
}
return instance; // 不看做是操作共享数据
}
}
- 线程的死锁问题
演示线程的死锁问题
1.死锁的理解: 不同的线程分别占用对方需要的同步资源不放弃 ,
都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2.说明:
1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2)使用同步时,要避免出现死锁
public class ThreadTest2 {
public static void main(String[] args) {
// 造两个常用类对象: 特殊字符串
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
// 多线程是同时执行的,谁先执行看CPU,所以才会打架
//匿名方式创建线程,只能用一次
new Thread(){
@Override
public void run() {
// 嵌套锁
// 手握s1锁
synchronized (s1){
s1.append("a");
s2.append(1);
// 死锁出现概率增加
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再握s2锁
synchronized (s2){
s1.append("b");
s2.append(2);
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 提供实现Runnable接口的匿名实现类的匿名对象
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append(4);
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
- Lock锁方式解决线程安全问题
解决线程安全问题的方式三: Lock锁 --> jdk5.0新增
相同: 二者都可以解决线程安全问题
不同: synchronized机制在执行完相应的同步代码后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的执行(unlock()),操作更灵活
2.优先使用顺序:
lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外),没有同步代码块灵活
如何解决线程安全问题? 有几种方式
- lock解决实现Runnable接口的线程安全问题
class Window5 implements Runnable{
// Lock是个接口,具体使用它的实现类ReentrantLock
// 在Runnable实现类中造一个ReentrantLock对象
// 公平锁: 当构造器参数为空时,则fair为false,当参数为true时,为公平的,排队的线程先来先服务
// 1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
private int ticket = 100;
// 重写Runnable接口的run方法
@Override
public void run() {
while (true){
// try finally是要保证lock后必须执行unlock
try {
// 2.调用锁定方法:lock(), 类似于线程获取了同步监视器,从lock()开始下面的代码被锁住了,保证这个过程中,他是单线程的,类似于同步代码块
// 当这些代码执行完的时候或是出现异常,也一定会执行finally里的代码
lock.lock(); // 手动上锁
if (ticket > 0){
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为: " + ticket);
ticket--;
}else{
break;
}
}finally {
// 3.调用解锁方法: unlock()
lock.unlock(); // 手动解锁;如果不执行这个方法,只上锁,之后的代码就都是单线程了,并且可能会造成饥饿,一直不结束
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window5 w = new Window5();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- lock解决继承Thread类的线程安全问题
class Window6 extends Thread{
private static int ticket = 100; // 将属性静态化,多个窗口共卖一百张票
static ReentrantLock lock = new ReentrantLock(); // 将锁静态化,多个线程共用同一把锁
@Override
public void run() {
while (true){
try {
lock.lock();
if (ticket > 0){
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为: " + ticket);
ticket--;
}else{
break;
}
}finally{
lock.unlock();
}
}
}
}
public class LockTest2 {
public static void main(String[] args) {
Window6 t1 = new Window6();
Window6 t2 = new Window6();
Window6 t3 = new Window6();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- lock处理实现Runnable接口的线程安全问题
class Window6 extends Thread{
private static int ticket = 100; // 多个窗口共卖一百张票
static ReentrantLock lock = new ReentrantLock(); // 将锁静态化,多个线程共用同一把锁
@Override
public void run() {
while (true){
try {
lock.lock();
if (ticket > 0){
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为: " + ticket);
ticket--;
}else{
break;
}
}finally{
lock.unlock();
}
}
}
}
public class LockTest2 {
public static void main(String[] args) {
Window6 t1 = new Window6();
Window6 t2 = new Window6();
Window6 t3 = new Window6();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 同步机制的练习
银行有一个账户
有两个储户分别向同一个账户存3000元,每次存1000,存3次.每次存完打印张华余额.
分析:
1.是否是多线程问题? 是,有两个储户线程
2.是否线程安全取决于有无共享数据,有共享数据: 账户(余额)
3.有线程安全问题
4.如何解决线程安全问题? 同步机制: 有三种方式
// 造一个class,专门作为账户
class Account{
private double balance;//余额
// 构造器初始化余额
public Account(double balance){
this.balance = balance;
}
// 造存钱方法
// 存在的安全问题: 当一个线程先执行存钱后,阻塞了没来得及输出,另一个线程也进来了也执行存钱,第一个线程醒了,就会输出两个线程存钱的余额
// 同步监视器能用this的原因: 此时的this不是多个Customer对象,而是共用的唯一一个Account对象,两个Customer又共用同一个Account
public synchronized void deposit(double amt){
if (amt > 0){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
balance += amt; // 余额增加,这里也相当于操纵共享数据
System.out.println(Thread.currentThread().getName() + "存钱成功,余额为: " + balance);
}
}
}
// 造一个线程客户类继承于Thread类
class Customer extends Thread{
// 将账户作为客户的属性,体现账户共享
private Account acct;
// 用构造器初始化属性
public Customer(Account acct){
this.acct = acct;//对象作为属性
}
// 重写Thread类的run方法
@Override
public void run() {
// 存三次钱
for (int i = 0; i < 3; i++) {
// 让账户加钱
acct.deposit(1000); // 对象调方法
}
}
}
public class AccountTest {
public static void main(String[] args) {
// 创建一个账户对象
Account acct = new Account(0);// 假设初值为0
// 此时两个客户就共用一个账户了
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
// 改名
c1.setName("甲");
c2.setName("乙");
// 启动线程,启动后就要调用重写Thread类的run方法
c1.start();
c2.start();
}
}
8.5.线程的通信
线程通信的例子: 用两个线程打印1~100.线程一,线程二 交替打印
涉及到的三个方法:
- wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.意味着其他线程可以拿到同步监视器,进入同步代码块
- notify(): 一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait,就唤醒优先级高的那个.总之只能唤醒一个
- notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器.所以他们才只能在上面两种情况内
否则会出现IllegalMonitorStateException异常
lock方法需要Condition来实现线程通信
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Ojbect类中.
同步监视器可以用任何对象充当,而这三种方法又必须要拿该对象调用,那得保证任何一个对象都得有这些方法,所以这些方法就在Ojbect类中定义
// 三个方法的调用必须在同步方法或同步代码块当中,包含在lock内也不行
class Num implements Runnable {
private int num = 1;//共享数据
// 重写run方法
@Override
public void run() {
while (true) {
// synchronized同步代码块
synchronized (this) { // this(同步监视器)为当前Num类的对象,唯一
// 调用该方法的线程可以唤醒另一个被阻塞的线程,如果有多个线程被wait,就唤醒优先级高(大概率被CPU分配到资源)的那个.
this.notify();// 省略了this
if (num <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
// 一个线程打印完后应该阻塞一下,另一个线程才能进来
// 调用如下wait方法后释放资源,进入等待池,会释放同步锁
try {
this.wait(); // 省略了this
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ConnectionTest {
public static void main(String[] args) {
Num n = new Num();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
- 面试题: sleep()和wait()的异同?
1.相同点: 一旦执行方法,都可以是的当前的线程进入阻塞状态,都需要处理异常
2.不同点:
①两个方法声明的位置不同:
sleep()声明在Thread类中
wait()声明在Ojbect类中
②调用的要求不同: sleep()可以在任何需要的场景下调用. wait()必须使用在同步代码块或同步方法中
③关于是否释放同步监视器: 如果两个方法都用在同步代码块或同步方法中,sleep()不会释放锁(同步监视器),wait()会释放锁
④sleep()会自动唤醒,wait()不会自动唤醒,需要notify()来唤醒
- 线程通信的应用: 生产者/消费者问题
分析:
1.是否为多线程问题? 是,生产者线程,消费者线程
2.是否有共享数据: 店员(或产品)
3.如何解决线程的安全问题? 同步机制,有三种方法
4.涉及到线程通信
// 店员类: 把店员改为为产品更好理解
class Clerk{
// 可以理解为店员记账
// 产品属性
private int productCount = 0;
// 获取产品
public synchronized void produceProduct(){
if (productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + " 开始生产第: " + productCount + "个产品");
// 生产者生产出一件商品就可以唤醒消费者
notify();
}else{
try {
wait(); // 可省略this
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
// 销售产品
public synchronized void comsumeProduct(){
if (productCount > 0){
System.out.println(Thread.currentThread().getName() + ": 消费第" + productCount + "个产品");
productCount--;
// 只要消费了一个商品,就可以把生产者唤醒
notify();
}else{
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
// 生产者类
class Producer extends Thread{
private Clerk clerk;//店员对象作为共享属性
// 构造器,共享店员
public Producer(Clerk clerk){
this.clerk = clerk;
}
// 重写run方法
@Override
public void run() {
System.out.println(getName() + " : 开始生产产品...");
while (true){
try {
Thread.sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
// 通过店员调用生产方法
clerk.produceProduct();
}
}
}
// 消费者类
class Comsumer extends Thread{
private Clerk clerk;//店员对象作为共享属性
// 构造器,共享店员
public Comsumer(Clerk clerk){
this.clerk = clerk;
}
// 重写run方法
@Override
public void run() {
System.out.println(getName() + ": 开始消费产品...");
while (true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 店员调用消费方法
clerk.comsumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Comsumer c1 = new Comsumer(clerk);
p1.setName("生产者1");
c1.setName("消费者1");
p1.start();
c1.start();
}
}
8.6.创建多线程的方式三: 实现Callable接口
创建线程的方式三: 实现Callable接口. -- jdk5.0新增
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()可以有返回值的
2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
3.Callable是支持泛型的
// 创建一个实现Callable接口的实现类
class NumThread implements Callable{
// 实现(重写)Callable接口的call(回调)方法,将此线程要执行的操作声明在call()中,同时该方法可以有返回值,如果不要返回可以return null
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;// 相当于把int转换为Integer:自动装箱,然后Integer作为子类赋给Object,体现多态性
}
}
public class ThreadNew {
public static void main(String[] args) {
// 3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
// 实现了Callable接口但没有Thread类作为父类,所以不能直接调用start方法
// 要用线程要借用FutureTask类
// 4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);// 其中有一个构造器要传一个实现Callable接口的实现类的对象
// 启动线程一定会newThread对象并且调用start方法
// futureTask实现的接口继承了Runnable接口,相当于也实现了Runnable接口,所以这里体现多态性: Runnable target = new futureTask();
// start后线程才会进入就绪状态
// 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start(); // Thread构造器实参可以放futureTask不会报错,因为futureTask同时实现了Runnable和Future接口
// 调这个对象的get方法,返回一个值
try {
// 6.获取Callable中call方法的返回值
// get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();//调方法返回值用一个变量接收,该变量的值就是call方法return的值
System.out.println("总和为: " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 新增方式二: 使用线程池
创建线程的方式四: 使用线程池
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理: 可以设置线程池的属性,限制线程池的创建,包括对线程池的维护
corePoolSize:核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止
面试题: 创建多线程有几种方式: 有四种
// 创建一个线程类
class NumberThread implements Runnable{
// 重写run方法,遍历1~100的偶数
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
// 同个线程池的其他线程类
class NumberThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1.提供指定线程数的线程池
// Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
// 调用工具类中的静态方法,可重用固定线程数的线程池
// 返回值是线程池ExecutorService接口类型的实现(子)类(ThreadPoolExecutor)的对象,体现多态性,这里不是new对象
ExecutorService service = Executors.newFixedThreadPool(10); // 造了一个线程池线程数为10
// 设置线程池的属性
System.out.println(service.getClass());// 获取该对象是哪个类造的
// service是父类,service1是子类,父类要用子类中特有的方法,必须向下强转
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(15);// 类中的属性可以是变量,所以可以设置,接口中的属性只能是常量,不能设置
// service1.setKeepAliveTime();
// 调用执行方法,传入要执行的Runnable接口的实现类的对象,自然就知道线程的run方法要做的事
// 提供方法参数的目的主要是知道该线程要做什么
// 2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());// 没有返回值,适用与Runnable
// 创建新的线程
service.execute(new NumberThread1());
// service.submit(Callable callable);// 有返回值,适用于Callable,submit方法返回一个Callable类型的值,可以用FutureTask类型的对象接收,再调get()查看返回值
// 3.不用线程池可以关闭连接池
service.shutdown();
}
}
网友评论