Day24 --多线程(上)
来源:互联网 发布:qq输入法 linux 编辑:程序博客网 时间:2024/06/06 00:22
a.
线程
概述
* 线程是程序执行的一条路径,一个进程中可以包含多条线程
* 多线程并发执行可以提高程序执行效率,同时可以完成多项工作
* 每一个程序的运行,就可以看做是一个个进程,进程中包含多条线程
* 例如:360软件,同时杀毒,同时清理垃圾,同时优化系统硬盘
p.s.
进程:正在运行的程序(直译)
线程:进程中一个负责程序执行的控制单元(执行路径)
1. 一个进程中可以有多个执行路径,成为多线程
2. 一个进程中至少有一个线程
3. 开启多个线程是为了同时运行多部分代码,每个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
4. 多线程好处:解决了多部分代码同时运行的问题
5. 多线程弊端:线程开启过多,会导致运行效率降低(多线程执行代码会提高运行效率,但是开启过多个线程,容易导致cpu执行效率降低,有可能导致 系统应用无反应)
6. 其实,多个应用程序同时执行都是cpu在做着快速的切换完成的,这个切换是随机的。cpu的切换需要花费时间,从而导致了效率的降低
7. JVM启动时启动了多条线程,至少有两条线程是我们应该知道的:
1. 负责垃圾回收的线程
2. 执行main函数的线程,该线程的任务代码都定义在main函数中
System类中的gc方法告诉垃圾回收器调用finalize方法,但不一定执行
应用场景
* 录屏软件共享给多台电脑使用
* QQ同时多人视频
* 服务器同时接受多个客户端请求
原理
* 因为cpu在做着高效的切换,因为时间间隔短,所以我们因为是同时执行的,多线程的执行具有 随机性 和 延迟性
注意
* 同一个线程不能重复启动,会报一个 IllegalThreadStateException 线程状态非法异常
多线程并发和并行区别
概述
* 并行:两个任务同时运行,就是A任务执行的同时,B任务也在执行(需要多核CPU)
* 并发:两个任务同时请求运行,而服务器CPU只能接受一个任务,所以就将这两个任务交替执行,由于执行时间间隔短,所以给人的感觉就是两个任务同时运行。
* cpu同时执行:QQ,Music,jvm,由于执行的时间间隔短,所以给人看出来就是同时执行3个任务,其实不然,而是cpu执行一下QQ,再执行一下Music,再执行一下jvm,由于cpu运行速度快,所以给人的感觉就是cpu一次性再执行3个任务,其实是随机执行的,一下执行这个,一下执行那个,以此类推随机的这种过程。
* 我同时和两个网友聊天,左手操控一台给A聊天,右手操控一台给B聊天,这叫 并行。
* 一台电脑,我和A发个消息后,立刻给B也发个消息,然后再跟A聊,再跟B聊,这叫 并发。
JAVA程序运行原理和JVM的启动是多线程吗?
* Java程序运行原理
* Java命令会将jvm虚拟机启动,启动虚拟机就等于启动一个程序,也就是启动了一个进程,该进程会自动启动一个 “主线程”,然后主线程去调用某个类的main方法。
* JVM的启动是多线程吗?
* JVM的启动至少启动了 垃圾回收机制线程 和 主线程,所以JVM是多线程的。
单线程执行:例如创建两个自定义类,分别调用AB方法,A方法调用完后,再调用B方法。 “自上而下,从左到右” 的一个执行过程。
而创建线程的目的就是为了开启另一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm虚拟机创建的主线程的任务都在主函数中,而自定义的线程,任务在哪里呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。整个任务就是通过Thread类中的run方法来实现的,也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码
开启线程就是为了运行指定的代码,所以只有继承了Thread类 或 实现Runnable接口,并重写run方法,将运行的代码定义在run方法中即可。
例如:
情况一:
1. 自定义类
2. 并在main主函数中创建该类对象,使用for循环来调用方法。
3. 然后在主函数中再写一个自己的for循环。
程序运行过程是:先调用该类对象的for循环中的方法,当方法调用完毕后,再执行main中的for循环,整个过程是:从左到右,自上而下 的执行。
p.s.
可以从单线程看出,只有上一个代码执行完后,下一个代码才有执行的机会
而创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个线程任务(执行路径的任务)
p.s.
jvm创建的主线程的任务都定义在了主函数中,而自定义的线程,它的任务又在哪里?
Thread类就是用于描述线程的类,线程是需要任务的,所以Thread类也有对任务的描述。
这个任务就是通过Thread类的run方法来体现的。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是该线程要运行的任务代码
p.s.
开启线程就是为了运行指定的代码,所有只有继承了Thread类,并重写run方法,将要运行的代码定义在run方法中即可
情况二:
1. 自定义类继承Thread类
2. 并在main主函数中创建该类对象,使用for循环来调用方法。
3. 且在主函数中写一个for循环。
程序的运行过程是:jvm启动后,两条线程是随机执行的,cpu一会执行一下自定义类中的for循环方法,再执行一下main中的for循环。整个过程是:随机交替执行的。
b.
多线程的实现
Thread类概述
* Thread是lang包下,使用时不用导包,直接父类Object,实现 Runnable 接口
* public class Thread extends Object implements Runnable{};
* 线程 是程序中的执行线程,JVM允许应用程序并发地执行多个执行线程
* 线程是有优先级的
* 实现方式有两种
实现方式一:
* 1、自定义类继承 Thread类
* 2、覆盖Thread中的run方法
* 3、将新线程要做的事情写到run方法中
* 4、创建Thread类的子类对象
* 5、调用Thread类的start()方法后会开启新线程,内部会自动执行自定义类继承Thread类重写的run()方法
实现方式二:
* 1、自定义类实现 Runnable 接口
* 2、覆盖接口中的run方法
* 3、将新线程要做的事情写在run方法中
* 4、创建Runnable的子类对象
* 5、创建Thread类对象,传入Runnable子类对象的引用给Thread类的函数构造,为什么?
* 因为线程的任务都封装在了Runnable接口子类对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务
* 6、调用 Thread类的start() 方法开启新线程,内部会自动调用 Runnable 的 run() 方法
实现Runnable的原理
1. 看Thread类的构造,传入Runnable接口的引用
2. 通过init()方法找到传递的target给成员变量的target赋值
3. 查看run方法,发现方法中有判断,如果target不为null,就会调用Runnable接口子类对象的run方法
两种方式的区别
* 从源码的区别来说:
* 继承Thread
* 由于子类重写了Thread类中的run方法, 所以当调用start()方法时,自己会直接去找子类的run方法
* 实现Runnable
* 构造方法中传入了Runnable的引用,成员变量记住了它,start()方法调用run()方法时,内部会判断成员变量Runnable的引用是否为空,不为空的时候,编译时会看父类Runnable中的run()方法, 运行的时候会执行子类的run()方法。这也是多态的特性之一:成员方法,编译看左(父),运行看右(子)。
* 从代码的关系上看:
* 继承Thread类
* 好处:可以直接使用Thread类中的方法,代码简单
* 弊端:如果该类已经有父类,就不能使用这个方法了,因为java中是单继承的。
* 实现Runnable接口
* 好处:即使自己定义的线程类有了父类也没有关系,因为有了父类也可以实现接口,因为Java中的接口是可以多实现的。
* 弊端:不能直接使用Thread类中的方法,必须要先获得Thread对象后并传入Runnable接口的引用后,才能得到Thread类中的方法,代码复杂
p.s.
实现Runnable接口的好处?
* 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成了对象
* 避免了java单继承的局限性,所以使用 实现Runnable接口的方式较为常用。
优先考虑使用哪种?
* 继承Thread类,代码简单,但如果该类有了父类,就不能使用该方法。
* 实现Runnable接口,代码扩展性强,代码虽然复杂,但即使该类有了父类,也能使用该方法。因为Java中接口是多实现的,但第一种情况不能用的时候,再考虑第二种,第二种相当于是第一种的一种替补补充。
使用匿名内部类来实现线程的两种方式
好处:
* 不用自定义类去实现Thread类 或 实现Runnable接口,而直接使用Thread中的方法。
* 继承Thread类
new Thread(){ //1、继承Thread类
@Override
public void run() { //2、重写run方法
for (int i = 0; i < 100000; i++) { //3、将新线程要做的事情写在run方法中
System.out.println("eeeeeeeeeeee");
}
}
}.start(); //4、开启线程
* 实现Runnable接口
new Thread(new Runnable() { //1、将Runnable的子类对象传递给Thread的构造方法
@Override
public void run() { //2、重写run方法
for (int i = 0; i < 100000; i++) { //3、将新线程要做的事情写在run方法中
System.out.println("bbbbbbbbbbbbbb");
}
}
}).start(); //4、开启线程
c.
多线程 --获取名字和设置名字
获取名称
* 通过this.getName()可以获取线程对象名称
* 线程的默认命名规则是:“Thread-编号”,编号是从0开始的。
设置名称 --两种方式
* 通过构造方法中传入 线程名称参数
* setName(String)方法可以设置线程对象名称
多线程 --获取当前正在运行的线程对象 Thread.currentThread();
* 返回当前正在执行的线程对象的引用:Thread.currentThread();
* 获取当前线程对象名称:Thread.currentThread().getName() --主线程对象也能获取
* (实现Runnable 接口的方式中,只能通过这种方式来获取当前线程名称,因为getName()是Thread的子类,实现Runnable 接口的方式不能直接使用getName()方法)
* 设置主线程名称:Thread.currentThread().setName("我是主线程");
d.
休眠线程
概述
* 休眠线程顾名思义,就是让当前线程睡上多常时间(暂停执行)
方法 -- 控制当前线程的休眠时间
* Thread.sleep(毫秒,纳秒);在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)
* Thread.sleep(long 毫秒); 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
公式
* 毫秒1秒 等于 1000毫秒
* 1000毫秒 等于 1秒
* 1秒 等于 1000 * 1000 * 1000纳秒
1秒 = 1000 毫秒
1毫秒 = 1000 微秒
1微秒 = 1000 纳秒
1纳秒 = 1000 皮秒
注意:
Thread.sleep(long time); 有异常,只能try/cath,原因如下:
* 异常注意事项之一:
* 被重写的方法没有异常抛出,子类的方法绝对不可以抛出异常的,如果子类方法中有异常,那么
* 子类只能try, 不能抛(父类没有抛出异常,子类覆盖后绝对也不能抛出,只能try/catch)
sleep() 和 wait() 方法区别
* sleep(); 暂停执行,不用唤醒,时间一过,自动放行。
* wait(); 等待执行,需要唤醒,如果没有被唤醒,那么一直处于等待状态。
守护线程
概述
* setDaemon(); 设置一个线程为守护线程,该线程不会单独执行,当其他非守护线程执行完后,Java虚拟机自动退出
* 该线程不会单独执行,当所有的非守护线程执行结束,守护线程会自动关闭。
* 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
* 该方法必须在启动线程前调用。
方法
* public final void setDaemon(boolean on);
* 参数: on - 如果为 true,则设置该线程为守护线程。
例子:
* 守护线程:車马相士炮
* 非守护线程:老帅 ,一旦老帅被将军,该棋结束(jvm退出)
注意:
* 将一个线程设置为守护线程,当非守护线程结束后,有时候守护线程不会立刻马上结束。
因为如果该守护线程是一个50次的for循环代码,可能非守护线程执行完后,守护线程会再 执行几次(肯定不会执行完50次),再jvm退出。
jvm退出也有一个过程的。在jvm退出的过程中,守护线程可能被执行几次,这样有可能的。
应用场景代码:
当QQ窗口正在传送数据,而此时将QQ主界面关闭,QQ传递数据的窗口不能立马结束,会有那么一点点的时间
间隔后再退出(可能正在往对方电脑发送数据包),这就是守护线程的作用(应用)。
注意:
有时候守护线程代码和非守护线程代码会交替执行,但守护线程for循环代码不会被执行完(也就几次),而非守护线程for循环代码肯定会执行完毕。因为这是 多线程并发而导致的交替执行线程的结果。
加入线程
概述
* join(); 加入线程,又叫插入线程
* 首先是并发交替执行:当t2 for循环遇到i==2的时候,加入t1.join(),t2线程暂停,因为有t1.join()加入,所以只有当t1线程执行完后,才能执行t2线程。
* join(long 毫秒值); 加入线程(参数:含有时间段的)
* 首先 t1 t2 并发交替执行,当t2的i==2的时候,由于t1.join(10)加入线程,所以让t1执行10毫秒的时间后,t1和t2再并发交替执行
方法
* join(); 等待该线程终止。
* 当前线程暂停,等到指定的线程执行结束后,当前线程再继续。
* join(long millis); 等待该线程终止的时间最长为 millis 毫秒。
* 等待的毫秒值结束后(期间会执行另一条加入的线程[含有时间段的])再继续执行当前线程。
礼让线程
概述
理论上来说
* 由于多线程是并发交替执行的,所以礼让线程就 是当前要执行的线程让出cpu执行权,让另一条线程执行。
* 让出cpu资源
但实际上
* 这种礼让线程效果不明显
方法
* yield(); 暂停当前正在执行的线程对象,并执行其他线程。
设置线程的优先级
概述
* setPriority(int newPriority); 设置当前线程的优先级
* 参数有三个:1(最小优先级),5(标准优先级),10(最大优先级)
* 数字越大,越优先被执行,但效果也不是很明显
d.
同步
概述
* 多线程并发,我们希望在某段代码执行结束前,cpu不要做切换,就要用到同步
* 同步可以用到的关键字:synchronized
同步代码块
概述
* 什么情况下需要同步
* 当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中,CPU不要切换到其他线程,这个时候就需要同步
* 如果两段代码是同步的,那么同一时间内只能执行一段,在一段代码没有执行结束之前,是不会执行另一段代码的
同步代码块
* 使用synchronized(森困奈斯) 关键字加上一个锁对象来定义一段代码, 这就叫 同步代码块。
* 多个同步代码块如果使用相同的锁对象,那么他们就是同步的
注意
* 同步代码块, 锁机制,锁对象可以是任意对象
* 锁对象不能是匿名对象,因为匿名对象不是同一个对象
* 使用锁对象必须使用同一把锁,不然可以出现锁不住的情况
e.
同步方法
概述
* 当方法内部所有的代码都需要加同步的时候,就可以考虑使用同步方法。
* 使用 synchronzied 关键字来修饰一个方法,该方法中所有的代码都是同步的
* 锁对象可以是任意对象,但被锁的代码必须保证是同一把锁,不能使用匿名对象,因为匿名对象不是同一个对象
* 非静态同步方法的锁是:this 关键字
* 静态的同步方法的锁是:当前类的字节码对象 --类名.class
为什么静态的同步方法的锁对象为什么不能是this?
* 因为静态是优先于对象而先存在的,静态方法中是不能有this和super
* this只能随着对象的调用而存在,随着对象的消失而消失
使用
非静态方法 的 同步方法?
* 在非静态方法上加 synchronzied 关键字,该方法就变成 同步方法
* 那么非静态同步方法的锁对象是什么?
* this 关键字
关键代码 --【必看】
// 测试 非静态的 同步方法
class MySyn{
public synchronized void method1(){ //在非静态方法加上 synchronized 就变成同步方法
System.out.print("张");
System.out.print("学");
System.out.print("良");
System.out.println();
}
// 非静态 同步方法 的锁对象是什么? this关键字
public void method2(){
synchronized (this) { // this关键字
System.out.print("胡");
System.out.print("雪");
System.out.print("岩");
System.out.println();
}
}
public static void main(String[] args) {
MySyn syn = new MySyn();
new Thread(){ //开启第一条线程
public void run() {
while (true) {
syn.method1();
}
};
}.start();
new Thread(){ //开启第二条线程
public void run() {
while (true) {
syn.method2();
}
};
}.start();
}
静态方法 的 同步方法?
* 在静态方法上加 synchronzied 关键字,该方法就变成 同步方法
* 那么静态同步方法的锁对象是什么?
* 该类的字节码对象
关键代码 --【必看】
class MySyn_Static{ // 测试 静态的 同步方法
public static synchronized void print1(){ //静态方法 的同步方法
System.out.print("曹");
System.out.print("孟");
System.out.print("德");
System.out.println();
}
// 静态的同步方法的锁对象是什么? 该类的字节码对象
public static void print2(){
synchronized (MySyn_Static.class) { //该类的字节码对象
System.out.print("关");
System.out.print("云");
System.out.print("长");
System.out.println();
}
}
}
同步锁问题:
* 锁中可以放任意对象,但是也分情况:
* 当该方法 是非静态的 同步方法
* 锁对象只能是:this
* 当该方法 是静态的 同步方法
* 锁对象只能是:该类的字节码文件对象
* 静态方法的同步锁对象之所以不能放this, 是因为静态优先于对象先存在。
* this只能随着对象的调用而存在,随着对象的消失而消失
* 静态方法中不能有this 和 super
f.
线程安全问题
概述
* 当多线程并发操作同一个数据时,有可能出现线程安全问题
* 可以使用 同步技术(synchronized, 同步方法,同步锁等) 来解决这个问题,把操作的同一个数据的代码进行同步,不要多个线程一起操作
p.s.
线程安全问题产生的原因?
1. 多个线程在操作同一个共享的数据
2. 操作共享数据的线程代码有多条
3. 当一个线程在执行操作共享数据的多条代码过程中,其他线程也参与了运算,这就会导致线程安全问题的产生
需求:铁路售票,一共100张,通过四个窗口卖完
多线程模拟买票可能会出现负数以及重复值:
//多线程并发操作同一数据,就有可能出问题。
出现负数:
关键点: 当ticket=1的时候,这个时候如果四个线程对象都进来了,就会出现负数
出现负数票:0, -1, -2的原因:
Thread-0通过if判断后,在执行到 “num--”语句之前,num此时仍等于1,但cpu切换到Thread-1, Thread-2, Thread-3之后, 这些线程依然可以通过if判断,从而执行了 “num--”的操作,所以就出现了 0, -1, -2的情况
出现重复值:
关键点:ticket--
ticket-- 相当于 ticket = ticket - 1; 其实做了三件事情。
A: 读取ticket的值。
B:修改ticket的值。
C:讲修改后的值重新赋值给ticket。
当A或者B都执行完的时候,还没有来得及执行C,这个时候如果别的线程对象抢到资源了,打印的就是重复值。
Collections.synchronizedXxx(); //可以将线程不安全的类,变成线程安全的类。
p.s.
线程安全问题的解决方案:
* 就是将多条操作共享数据的线程代码给封装起来,当有线程在执行这些代码的时候,其他线程就不参与运算,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
在java中,使用同步代码块就可以解决这个问题:
同步代码块格式:
synchronized(任意对象){
需要被同步的代码;
}
同步的好处:解决了线程安全问题
同步的弊端:当线程相当多时,每个线程都会去判断同步上的锁,这样很耗费资源,无形中就降低了程序的运行效率
同步的前提:必须多个线程,并使用同一个锁
安全问题的另一种解决方案:同步代码块
* 格式:在函数上加 synchronized修饰符即可。
p.s.
同步函数和同步代码块的区别
* 同步函数的锁是固定的this
* 同步代码块的锁是任意的对象
建议使用同步代码块
由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用当前类名.class表示,也可用getClass()方法来获取
p.s.
多线程并发,有多段代码同时执行,我们想在执行某一段代码的同时,其他线程不参与进来执行,这个时候就需要用到 《同步》 来解决。有两种解决方案: 同步代码块 和 同步方法
同步代码块:
格式:synchronized(this){}
同步方法:
非静态同步方法
格式:public synchronized void method(){}
非静态同步方法的锁对象是:this
静态同步方法
格式:public static synchronized void method(){}
静态同步方法的锁对象: 该类的字节码文件对象
代码
package com.day24.c.thread.mysynchronize;
public class Demo03_多线程_线程的安全问题 {
public static void main(String[] args) {
// 创建四个对象,就是开 四个售票窗口
SellTicket t1 = new SellTicket();
SellTicket t2 = new SellTicket();
SellTicket t3 = new SellTicket();
SellTicket t4 = new SellTicket();
// 设置每个窗口的名字
t1.setName("1号");
t2.setName("2号");
t3.setName("3号");
t4.setName("4号");
// 开启每个线程(窗口)开始同时售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
// 卖票
class SellTicket extends Thread{
// 1)设置当前总票数为100张
public static int TicketNum = 100; //设置为静态,是为了无论创建了多少个对象,都共享这一个属性值
// 也可以给锁对象 任意对象,但是该任意对象必须是静态的。
static Object object = new Object();
// 2)重写Thread中的run方法,因为让新线程执行的代码都是要放在run方法中的。
@Override
public void run() {
// 3)使用white循环,来模拟一直卖票过程
while(true){
// 6)加锁,因为只有加锁后,不会有线程安全问题的存在。
// 方式有两种,一是 this,二是 该类字节码对象(建议)
// 也可以给锁对象 任意对象,但是该任意对象必须是静态的。
synchronized (SellTicket.class) { //因为字节码对象只有一个
// synchronized (object) { // 如果用引用数据类型成员变量当做锁,那么该对象必须是静态的 --这种方式没有直接给 该类字节码对象方便
// 4)当票数等于0的时候,跳出该while循环
if(TicketNum == 0){
break;
}
// 5)中间还有卖票的其他操作,如联网,查询剩余票数等。
// 这些功能代码暂时没有,先用 sleep(10)来代替,因为这些功能执行也是需要时间的,所
// 以使用sleep(10)给设置10毫秒的时间,就等于这些功能的运行过了
// 6)因为该功能没有加上锁,所以存在线程安全问题,使用要加上锁 --对操作的同一个数据进行加锁操作,可避免线程安全问题出现
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前 "+this.getName()+" 窗口,正在卖 "+ (TicketNum--) +" 号票");
}
}
}
}
g.
火车站卖票的例子用实现Runnable接口 【面试】
需求:铁路售票,一共100张,通过四个窗口卖完
--实现 Runnable接口 的方式来做
代码
public class Demo04_多线程_线程的安全问题_火车售票_自定义类实现Runnab接口_的方法来实现 {
public static void main(String[] args) {
// 创建Runnable的子类对象
MySys_Runnable mr = new MySys_Runnable();
// 开启四个窗口(其实是开启 四条线程)
Thread t1 = new Thread(mr);
t1.setName("1号");
t1.start();
Thread t2 = new Thread(mr);
t2.setName("2号");
t2.start();
Thread t3 = new Thread(mr);
t3.setName("3号");
t3.start();
Thread t4 = new Thread(mr);
t4.setName("4号");
t4.start();
}
}
class MySys_Runnable implements Runnable {
public static int numTicket = 100; //总票数
@Override
public void run() {
while(true){ //一直循环出售
synchronized (MySys_Runnable.class) { //添加同步锁
if( numTicket == 0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前 "+Thread.currentThread().getName()+" 窗口 正在出售第 "+ (numTicket--) +" 号票中...");
}
}
}
}
h.
死锁
概述
* 多线程同步的时候,如果同步代码嵌套,使用相同的锁,就会出现死锁现象
* 所以 同步代码块 尽量不要嵌套使用
代码
public class Demo05_多线程_死锁问题 {
private static String s1 ="筷子左"; // 定义筷子 来区分两把不同的 锁
private static String s2 ="筷子右"; // 定义筷子 来区分两把不同的 锁
public static void main(String[] args) {
new Thread("线程一 "){
public void run() {
while (true) {
synchronized (s1) {
System.out.println(this.getName()+" 拿到 "+s1+" 等待 "+s2);
synchronized (s2) { // 同步代码块的嵌套 会出现死锁现象
System.out.println(this.getName()+" 拿到 "+s2+" 开吃");
}
}
}
};
}.start();
new Thread("线程二 "){
public void run() {
while (true) {
synchronized (s2) {
System.out.println(this.getName()+" 拿到 "+s2+" 等待 "+s1);
synchronized (s1) { // 同步代码块的嵌套 会出现死锁现象
System.out.println(this.getName()+" 拿到 "+s1+" 开吃");
}
}
}
};
}.start();
// 运行结果:代码执行到一小半就不执行了,但是程序还没有停止(控制台
// 还运行着红方框),所以这个时候程序 就出现了 死锁状态,所以
// 我们在使用同步锁的时候,避免嵌套使用。
}
}
i.
回顾以前说过的线程安全问题
* 看源码:
* Vector,StringBuffer,Hashtable,Collections.synchroinzed(xxx)
* 查看源码可知,这些类中的某些方法 只要加上 synchroinzed 就证明该类是线程安全的,同步的。
* 具体的类
* Vector是线程安全的 --jdk1.0
* 格式:
* public synchronized void addElement(E obj) {}
* 是线程安全,同步,因为该类中 addElement(E obj)方法加了 synchroinzed,所以该方法是非静态的 同步方法
* ArrayList是线程不安全的 --jdk1.2
* 格式:
* public boolean add(E e) {}
* 是线程不安全,不同步。因为该类中的 add(E e)方法 没有加 synchroinzed
* StringBuffer是线程安全的 --jdk1.0
* 格式:
* public synchronized StringBuffer append(Object obj) {}
* 是线程安全,同步。因为该类中的 append(Object obj) 加了 synchroinzed,所以该方法是非静态的 同步方法
* StringBuilder是线程不安全的 --jdk1.5
* 格式:
* public StringBuilder append(Object obj){}
* 是线程不安全,不同步。 因为该类中的 append(Object obj) 没有加 synchroinzed
* Hashtable是线程安全的 --jdk1.0
* 格式:
* public synchronized V put(K key, V value){}
* 是线程安全的,同步。因为该类的put方法中加入了 synchroinzed,所以该方法是非静态的 同步方法
* HashMap是线程不安全的 --jdk1.2
* public V put(K key, V value) {}
* 是线程不安全的,不同步。 因为该类中的 put(K key, V value) 没有加 synchroinzed
Collections.synchroinzedXXX(xxx) 可以将线程不安全的设置为线程安全的(同步的)。
多线程 【面试题】
1. 什么是多线程的并发和并行?
并行:两个任务同时执行,在执行A任务的同时,也执行B任务。前提是:这需要多核CPU
并发:两个任务同时请求执行,但cpu只能执行一个,所以只能交替执行,由于交替执行的时间间隔太快,所以给人看起来是同时执行,但其实不是。
2. JVM的启动是多线程的吗?
jvm的启动是多线程的,至少启动了 垃圾回收线程 和 主线程
3. Java程序的运行原理
Java程序的启动,会先启动jvm虚拟机,再由jvm虚拟机去启动主线程,再由主线程去调用类的main函数
4. 多线程的实现之:实现Runnable接口的原理
其实是通过多态来实现的。
* 原理:
* 其实在Thread类里边有一个成员变量target,用来记录传过来的 Runnable接口的子类对象,
将传过来的对象,赋值给target,这样调用 Thread 类中的run()方法的时候,根据多态的原理,
* 编译看左边,运行看右边。实际调用的还是MyRunnable类的run()方法。
* 源码
class Thread {
private Runnable target;
public Thread(Runnable target) { //Runnable target = new MyRunnable();
this.target = target;
}
public void run() {
if (target != null) {
target.run();
}
}
}
5. 多线程的两种实现方式的区别:
继承 Thread类
* 代码简单,能够直接使用Thread中的方法
* 好处:因为是继承Thread类,所以可以直接使用Thread中的公共方法
* 弊端:如果类已经有了继承的其他类,就不能使用这个方法
实现 Runnable接口
* 代码功能扩展性强
* 好处:即使该类继承了其他类,也能使用这种方法,因为类是单继承,多实现的
* 弊端:不能直接使用Thread中的公共方法,所以必须要先获取到Thread类的对象后,才能使用到Thread中的方法,代码复杂
6. 线程默认优先级分为几个级别?
* 默认 5
* 范围:1 ~ 10
7. 线程的优先级别高,就代表该线程就会第一个执行吗?
* 不一定
* 线程的优先级越高代码该线程在一定程度上可以获取到更多的执行权,但不能完全代表该线程就会第一个执行
8. 同步代码块 和同步方法的锁对象分别是是什么?
非静态的同步代码块的锁对象是:this
静态的同步代码块的锁对象的是:该类的字节码对象 --类名.class
线程
概述
* 线程是程序执行的一条路径,一个进程中可以包含多条线程
* 多线程并发执行可以提高程序执行效率,同时可以完成多项工作
* 每一个程序的运行,就可以看做是一个个进程,进程中包含多条线程
* 例如:360软件,同时杀毒,同时清理垃圾,同时优化系统硬盘
p.s.
进程:正在运行的程序(直译)
线程:进程中一个负责程序执行的控制单元(执行路径)
1. 一个进程中可以有多个执行路径,成为多线程
2. 一个进程中至少有一个线程
3. 开启多个线程是为了同时运行多部分代码,每个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
4. 多线程好处:解决了多部分代码同时运行的问题
5. 多线程弊端:线程开启过多,会导致运行效率降低(多线程执行代码会提高运行效率,但是开启过多个线程,容易导致cpu执行效率降低,有可能导致 系统应用无反应)
6. 其实,多个应用程序同时执行都是cpu在做着快速的切换完成的,这个切换是随机的。cpu的切换需要花费时间,从而导致了效率的降低
7. JVM启动时启动了多条线程,至少有两条线程是我们应该知道的:
1. 负责垃圾回收的线程
2. 执行main函数的线程,该线程的任务代码都定义在main函数中
System类中的gc方法告诉垃圾回收器调用finalize方法,但不一定执行
应用场景
* 录屏软件共享给多台电脑使用
* QQ同时多人视频
* 服务器同时接受多个客户端请求
原理
* 因为cpu在做着高效的切换,因为时间间隔短,所以我们因为是同时执行的,多线程的执行具有 随机性 和 延迟性
注意
* 同一个线程不能重复启动,会报一个 IllegalThreadStateException 线程状态非法异常
多线程并发和并行区别
概述
* 并行:两个任务同时运行,就是A任务执行的同时,B任务也在执行(需要多核CPU)
* 并发:两个任务同时请求运行,而服务器CPU只能接受一个任务,所以就将这两个任务交替执行,由于执行时间间隔短,所以给人的感觉就是两个任务同时运行。
* cpu同时执行:QQ,Music,jvm,由于执行的时间间隔短,所以给人看出来就是同时执行3个任务,其实不然,而是cpu执行一下QQ,再执行一下Music,再执行一下jvm,由于cpu运行速度快,所以给人的感觉就是cpu一次性再执行3个任务,其实是随机执行的,一下执行这个,一下执行那个,以此类推随机的这种过程。
* 我同时和两个网友聊天,左手操控一台给A聊天,右手操控一台给B聊天,这叫 并行。
* 一台电脑,我和A发个消息后,立刻给B也发个消息,然后再跟A聊,再跟B聊,这叫 并发。
JAVA程序运行原理和JVM的启动是多线程吗?
* Java程序运行原理
* Java命令会将jvm虚拟机启动,启动虚拟机就等于启动一个程序,也就是启动了一个进程,该进程会自动启动一个 “主线程”,然后主线程去调用某个类的main方法。
* JVM的启动是多线程吗?
* JVM的启动至少启动了 垃圾回收机制线程 和 主线程,所以JVM是多线程的。
单线程执行:例如创建两个自定义类,分别调用AB方法,A方法调用完后,再调用B方法。 “自上而下,从左到右” 的一个执行过程。
而创建线程的目的就是为了开启另一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm虚拟机创建的主线程的任务都在主函数中,而自定义的线程,任务在哪里呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。整个任务就是通过Thread类中的run方法来实现的,也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码
开启线程就是为了运行指定的代码,所以只有继承了Thread类 或 实现Runnable接口,并重写run方法,将运行的代码定义在run方法中即可。
例如:
情况一:
1. 自定义类
2. 并在main主函数中创建该类对象,使用for循环来调用方法。
3. 然后在主函数中再写一个自己的for循环。
程序运行过程是:先调用该类对象的for循环中的方法,当方法调用完毕后,再执行main中的for循环,整个过程是:从左到右,自上而下 的执行。
p.s.
可以从单线程看出,只有上一个代码执行完后,下一个代码才有执行的机会
而创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个线程任务(执行路径的任务)
p.s.
jvm创建的主线程的任务都定义在了主函数中,而自定义的线程,它的任务又在哪里?
Thread类就是用于描述线程的类,线程是需要任务的,所以Thread类也有对任务的描述。
这个任务就是通过Thread类的run方法来体现的。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是该线程要运行的任务代码
p.s.
开启线程就是为了运行指定的代码,所有只有继承了Thread类,并重写run方法,将要运行的代码定义在run方法中即可
情况二:
1. 自定义类继承Thread类
2. 并在main主函数中创建该类对象,使用for循环来调用方法。
3. 且在主函数中写一个for循环。
程序的运行过程是:jvm启动后,两条线程是随机执行的,cpu一会执行一下自定义类中的for循环方法,再执行一下main中的for循环。整个过程是:随机交替执行的。
b.
多线程的实现
Thread类概述
* Thread是lang包下,使用时不用导包,直接父类Object,实现 Runnable 接口
* public class Thread extends Object implements Runnable{};
* 线程 是程序中的执行线程,JVM允许应用程序并发地执行多个执行线程
* 线程是有优先级的
* 实现方式有两种
实现方式一:
* 1、自定义类继承 Thread类
* 2、覆盖Thread中的run方法
* 3、将新线程要做的事情写到run方法中
* 4、创建Thread类的子类对象
* 5、调用Thread类的start()方法后会开启新线程,内部会自动执行自定义类继承Thread类重写的run()方法
实现方式二:
* 1、自定义类实现 Runnable 接口
* 2、覆盖接口中的run方法
* 3、将新线程要做的事情写在run方法中
* 4、创建Runnable的子类对象
* 5、创建Thread类对象,传入Runnable子类对象的引用给Thread类的函数构造,为什么?
* 因为线程的任务都封装在了Runnable接口子类对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务
* 6、调用 Thread类的start() 方法开启新线程,内部会自动调用 Runnable 的 run() 方法
实现Runnable的原理
1. 看Thread类的构造,传入Runnable接口的引用
2. 通过init()方法找到传递的target给成员变量的target赋值
3. 查看run方法,发现方法中有判断,如果target不为null,就会调用Runnable接口子类对象的run方法
两种方式的区别
* 从源码的区别来说:
* 继承Thread
* 由于子类重写了Thread类中的run方法, 所以当调用start()方法时,自己会直接去找子类的run方法
* 实现Runnable
* 构造方法中传入了Runnable的引用,成员变量记住了它,start()方法调用run()方法时,内部会判断成员变量Runnable的引用是否为空,不为空的时候,编译时会看父类Runnable中的run()方法, 运行的时候会执行子类的run()方法。这也是多态的特性之一:成员方法,编译看左(父),运行看右(子)。
* 从代码的关系上看:
* 继承Thread类
* 好处:可以直接使用Thread类中的方法,代码简单
* 弊端:如果该类已经有父类,就不能使用这个方法了,因为java中是单继承的。
* 实现Runnable接口
* 好处:即使自己定义的线程类有了父类也没有关系,因为有了父类也可以实现接口,因为Java中的接口是可以多实现的。
* 弊端:不能直接使用Thread类中的方法,必须要先获得Thread对象后并传入Runnable接口的引用后,才能得到Thread类中的方法,代码复杂
p.s.
实现Runnable接口的好处?
* 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成了对象
* 避免了java单继承的局限性,所以使用 实现Runnable接口的方式较为常用。
优先考虑使用哪种?
* 继承Thread类,代码简单,但如果该类有了父类,就不能使用该方法。
* 实现Runnable接口,代码扩展性强,代码虽然复杂,但即使该类有了父类,也能使用该方法。因为Java中接口是多实现的,但第一种情况不能用的时候,再考虑第二种,第二种相当于是第一种的一种替补补充。
使用匿名内部类来实现线程的两种方式
好处:
* 不用自定义类去实现Thread类 或 实现Runnable接口,而直接使用Thread中的方法。
* 继承Thread类
new Thread(){ //1、继承Thread类
@Override
public void run() { //2、重写run方法
for (int i = 0; i < 100000; i++) { //3、将新线程要做的事情写在run方法中
System.out.println("eeeeeeeeeeee");
}
}
}.start(); //4、开启线程
* 实现Runnable接口
new Thread(new Runnable() { //1、将Runnable的子类对象传递给Thread的构造方法
@Override
public void run() { //2、重写run方法
for (int i = 0; i < 100000; i++) { //3、将新线程要做的事情写在run方法中
System.out.println("bbbbbbbbbbbbbb");
}
}
}).start(); //4、开启线程
c.
多线程 --获取名字和设置名字
获取名称
* 通过this.getName()可以获取线程对象名称
* 线程的默认命名规则是:“Thread-编号”,编号是从0开始的。
设置名称 --两种方式
* 通过构造方法中传入 线程名称参数
* setName(String)方法可以设置线程对象名称
多线程 --获取当前正在运行的线程对象 Thread.currentThread();
* 返回当前正在执行的线程对象的引用:Thread.currentThread();
* 获取当前线程对象名称:Thread.currentThread().getName() --主线程对象也能获取
* (实现Runnable 接口的方式中,只能通过这种方式来获取当前线程名称,因为getName()是Thread的子类,实现Runnable 接口的方式不能直接使用getName()方法)
* 设置主线程名称:Thread.currentThread().setName("我是主线程");
d.
休眠线程
概述
* 休眠线程顾名思义,就是让当前线程睡上多常时间(暂停执行)
方法 -- 控制当前线程的休眠时间
* Thread.sleep(毫秒,纳秒);在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)
* Thread.sleep(long 毫秒); 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
公式
* 毫秒1秒 等于 1000毫秒
* 1000毫秒 等于 1秒
* 1秒 等于 1000 * 1000 * 1000纳秒
1秒 = 1000 毫秒
1毫秒 = 1000 微秒
1微秒 = 1000 纳秒
1纳秒 = 1000 皮秒
注意:
Thread.sleep(long time); 有异常,只能try/cath,原因如下:
* 异常注意事项之一:
* 被重写的方法没有异常抛出,子类的方法绝对不可以抛出异常的,如果子类方法中有异常,那么
* 子类只能try, 不能抛(父类没有抛出异常,子类覆盖后绝对也不能抛出,只能try/catch)
sleep() 和 wait() 方法区别
* sleep(); 暂停执行,不用唤醒,时间一过,自动放行。
* wait(); 等待执行,需要唤醒,如果没有被唤醒,那么一直处于等待状态。
守护线程
概述
* setDaemon(); 设置一个线程为守护线程,该线程不会单独执行,当其他非守护线程执行完后,Java虚拟机自动退出
* 该线程不会单独执行,当所有的非守护线程执行结束,守护线程会自动关闭。
* 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
* 该方法必须在启动线程前调用。
方法
* public final void setDaemon(boolean on);
* 参数: on - 如果为 true,则设置该线程为守护线程。
例子:
* 守护线程:車马相士炮
* 非守护线程:老帅 ,一旦老帅被将军,该棋结束(jvm退出)
注意:
* 将一个线程设置为守护线程,当非守护线程结束后,有时候守护线程不会立刻马上结束。
因为如果该守护线程是一个50次的for循环代码,可能非守护线程执行完后,守护线程会再 执行几次(肯定不会执行完50次),再jvm退出。
jvm退出也有一个过程的。在jvm退出的过程中,守护线程可能被执行几次,这样有可能的。
应用场景代码:
当QQ窗口正在传送数据,而此时将QQ主界面关闭,QQ传递数据的窗口不能立马结束,会有那么一点点的时间
间隔后再退出(可能正在往对方电脑发送数据包),这就是守护线程的作用(应用)。
注意:
有时候守护线程代码和非守护线程代码会交替执行,但守护线程for循环代码不会被执行完(也就几次),而非守护线程for循环代码肯定会执行完毕。因为这是 多线程并发而导致的交替执行线程的结果。
加入线程
概述
* join(); 加入线程,又叫插入线程
* 首先是并发交替执行:当t2 for循环遇到i==2的时候,加入t1.join(),t2线程暂停,因为有t1.join()加入,所以只有当t1线程执行完后,才能执行t2线程。
* join(long 毫秒值); 加入线程(参数:含有时间段的)
* 首先 t1 t2 并发交替执行,当t2的i==2的时候,由于t1.join(10)加入线程,所以让t1执行10毫秒的时间后,t1和t2再并发交替执行
方法
* join(); 等待该线程终止。
* 当前线程暂停,等到指定的线程执行结束后,当前线程再继续。
* join(long millis); 等待该线程终止的时间最长为 millis 毫秒。
* 等待的毫秒值结束后(期间会执行另一条加入的线程[含有时间段的])再继续执行当前线程。
礼让线程
概述
理论上来说
* 由于多线程是并发交替执行的,所以礼让线程就 是当前要执行的线程让出cpu执行权,让另一条线程执行。
* 让出cpu资源
但实际上
* 这种礼让线程效果不明显
方法
* yield(); 暂停当前正在执行的线程对象,并执行其他线程。
设置线程的优先级
概述
* setPriority(int newPriority); 设置当前线程的优先级
* 参数有三个:1(最小优先级),5(标准优先级),10(最大优先级)
* 数字越大,越优先被执行,但效果也不是很明显
d.
同步
概述
* 多线程并发,我们希望在某段代码执行结束前,cpu不要做切换,就要用到同步
* 同步可以用到的关键字:synchronized
同步代码块
概述
* 什么情况下需要同步
* 当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中,CPU不要切换到其他线程,这个时候就需要同步
* 如果两段代码是同步的,那么同一时间内只能执行一段,在一段代码没有执行结束之前,是不会执行另一段代码的
同步代码块
* 使用synchronized(森困奈斯) 关键字加上一个锁对象来定义一段代码, 这就叫 同步代码块。
* 多个同步代码块如果使用相同的锁对象,那么他们就是同步的
注意
* 同步代码块, 锁机制,锁对象可以是任意对象
* 锁对象不能是匿名对象,因为匿名对象不是同一个对象
* 使用锁对象必须使用同一把锁,不然可以出现锁不住的情况
e.
同步方法
概述
* 当方法内部所有的代码都需要加同步的时候,就可以考虑使用同步方法。
* 使用 synchronzied 关键字来修饰一个方法,该方法中所有的代码都是同步的
* 锁对象可以是任意对象,但被锁的代码必须保证是同一把锁,不能使用匿名对象,因为匿名对象不是同一个对象
* 非静态同步方法的锁是:this 关键字
* 静态的同步方法的锁是:当前类的字节码对象 --类名.class
为什么静态的同步方法的锁对象为什么不能是this?
* 因为静态是优先于对象而先存在的,静态方法中是不能有this和super
* this只能随着对象的调用而存在,随着对象的消失而消失
使用
非静态方法 的 同步方法?
* 在非静态方法上加 synchronzied 关键字,该方法就变成 同步方法
* 那么非静态同步方法的锁对象是什么?
* this 关键字
关键代码 --【必看】
// 测试 非静态的 同步方法
class MySyn{
public synchronized void method1(){ //在非静态方法加上 synchronized 就变成同步方法
System.out.print("张");
System.out.print("学");
System.out.print("良");
System.out.println();
}
// 非静态 同步方法 的锁对象是什么? this关键字
public void method2(){
synchronized (this) { // this关键字
System.out.print("胡");
System.out.print("雪");
System.out.print("岩");
System.out.println();
}
}
public static void main(String[] args) {
MySyn syn = new MySyn();
new Thread(){ //开启第一条线程
public void run() {
while (true) {
syn.method1();
}
};
}.start();
new Thread(){ //开启第二条线程
public void run() {
while (true) {
syn.method2();
}
};
}.start();
}
静态方法 的 同步方法?
* 在静态方法上加 synchronzied 关键字,该方法就变成 同步方法
* 那么静态同步方法的锁对象是什么?
* 该类的字节码对象
关键代码 --【必看】
class MySyn_Static{ // 测试 静态的 同步方法
public static synchronized void print1(){ //静态方法 的同步方法
System.out.print("曹");
System.out.print("孟");
System.out.print("德");
System.out.println();
}
// 静态的同步方法的锁对象是什么? 该类的字节码对象
public static void print2(){
synchronized (MySyn_Static.class) { //该类的字节码对象
System.out.print("关");
System.out.print("云");
System.out.print("长");
System.out.println();
}
}
}
同步锁问题:
* 锁中可以放任意对象,但是也分情况:
* 当该方法 是非静态的 同步方法
* 锁对象只能是:this
* 当该方法 是静态的 同步方法
* 锁对象只能是:该类的字节码文件对象
* 静态方法的同步锁对象之所以不能放this, 是因为静态优先于对象先存在。
* this只能随着对象的调用而存在,随着对象的消失而消失
* 静态方法中不能有this 和 super
f.
线程安全问题
概述
* 当多线程并发操作同一个数据时,有可能出现线程安全问题
* 可以使用 同步技术(synchronized, 同步方法,同步锁等) 来解决这个问题,把操作的同一个数据的代码进行同步,不要多个线程一起操作
p.s.
线程安全问题产生的原因?
1. 多个线程在操作同一个共享的数据
2. 操作共享数据的线程代码有多条
3. 当一个线程在执行操作共享数据的多条代码过程中,其他线程也参与了运算,这就会导致线程安全问题的产生
需求:铁路售票,一共100张,通过四个窗口卖完
多线程模拟买票可能会出现负数以及重复值:
//多线程并发操作同一数据,就有可能出问题。
出现负数:
关键点: 当ticket=1的时候,这个时候如果四个线程对象都进来了,就会出现负数
出现负数票:0, -1, -2的原因:
Thread-0通过if判断后,在执行到 “num--”语句之前,num此时仍等于1,但cpu切换到Thread-1, Thread-2, Thread-3之后, 这些线程依然可以通过if判断,从而执行了 “num--”的操作,所以就出现了 0, -1, -2的情况
出现重复值:
关键点:ticket--
ticket-- 相当于 ticket = ticket - 1; 其实做了三件事情。
A: 读取ticket的值。
B:修改ticket的值。
C:讲修改后的值重新赋值给ticket。
当A或者B都执行完的时候,还没有来得及执行C,这个时候如果别的线程对象抢到资源了,打印的就是重复值。
Collections.synchronizedXxx(); //可以将线程不安全的类,变成线程安全的类。
p.s.
线程安全问题的解决方案:
* 就是将多条操作共享数据的线程代码给封装起来,当有线程在执行这些代码的时候,其他线程就不参与运算,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
在java中,使用同步代码块就可以解决这个问题:
同步代码块格式:
synchronized(任意对象){
需要被同步的代码;
}
同步的好处:解决了线程安全问题
同步的弊端:当线程相当多时,每个线程都会去判断同步上的锁,这样很耗费资源,无形中就降低了程序的运行效率
同步的前提:必须多个线程,并使用同一个锁
安全问题的另一种解决方案:同步代码块
* 格式:在函数上加 synchronized修饰符即可。
p.s.
同步函数和同步代码块的区别
* 同步函数的锁是固定的this
* 同步代码块的锁是任意的对象
建议使用同步代码块
由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用当前类名.class表示,也可用getClass()方法来获取
p.s.
多线程并发,有多段代码同时执行,我们想在执行某一段代码的同时,其他线程不参与进来执行,这个时候就需要用到 《同步》 来解决。有两种解决方案: 同步代码块 和 同步方法
同步代码块:
格式:synchronized(this){}
同步方法:
非静态同步方法
格式:public synchronized void method(){}
非静态同步方法的锁对象是:this
静态同步方法
格式:public static synchronized void method(){}
静态同步方法的锁对象: 该类的字节码文件对象
代码
package com.day24.c.thread.mysynchronize;
public class Demo03_多线程_线程的安全问题 {
public static void main(String[] args) {
// 创建四个对象,就是开 四个售票窗口
SellTicket t1 = new SellTicket();
SellTicket t2 = new SellTicket();
SellTicket t3 = new SellTicket();
SellTicket t4 = new SellTicket();
// 设置每个窗口的名字
t1.setName("1号");
t2.setName("2号");
t3.setName("3号");
t4.setName("4号");
// 开启每个线程(窗口)开始同时售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
// 卖票
class SellTicket extends Thread{
// 1)设置当前总票数为100张
public static int TicketNum = 100; //设置为静态,是为了无论创建了多少个对象,都共享这一个属性值
// 也可以给锁对象 任意对象,但是该任意对象必须是静态的。
static Object object = new Object();
// 2)重写Thread中的run方法,因为让新线程执行的代码都是要放在run方法中的。
@Override
public void run() {
// 3)使用white循环,来模拟一直卖票过程
while(true){
// 6)加锁,因为只有加锁后,不会有线程安全问题的存在。
// 方式有两种,一是 this,二是 该类字节码对象(建议)
// 也可以给锁对象 任意对象,但是该任意对象必须是静态的。
synchronized (SellTicket.class) { //因为字节码对象只有一个
// synchronized (object) { // 如果用引用数据类型成员变量当做锁,那么该对象必须是静态的 --这种方式没有直接给 该类字节码对象方便
// 4)当票数等于0的时候,跳出该while循环
if(TicketNum == 0){
break;
}
// 5)中间还有卖票的其他操作,如联网,查询剩余票数等。
// 这些功能代码暂时没有,先用 sleep(10)来代替,因为这些功能执行也是需要时间的,所
// 以使用sleep(10)给设置10毫秒的时间,就等于这些功能的运行过了
// 6)因为该功能没有加上锁,所以存在线程安全问题,使用要加上锁 --对操作的同一个数据进行加锁操作,可避免线程安全问题出现
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前 "+this.getName()+" 窗口,正在卖 "+ (TicketNum--) +" 号票");
}
}
}
}
g.
火车站卖票的例子用实现Runnable接口 【面试】
需求:铁路售票,一共100张,通过四个窗口卖完
--实现 Runnable接口 的方式来做
代码
public class Demo04_多线程_线程的安全问题_火车售票_自定义类实现Runnab接口_的方法来实现 {
public static void main(String[] args) {
// 创建Runnable的子类对象
MySys_Runnable mr = new MySys_Runnable();
// 开启四个窗口(其实是开启 四条线程)
Thread t1 = new Thread(mr);
t1.setName("1号");
t1.start();
Thread t2 = new Thread(mr);
t2.setName("2号");
t2.start();
Thread t3 = new Thread(mr);
t3.setName("3号");
t3.start();
Thread t4 = new Thread(mr);
t4.setName("4号");
t4.start();
}
}
class MySys_Runnable implements Runnable {
public static int numTicket = 100; //总票数
@Override
public void run() {
while(true){ //一直循环出售
synchronized (MySys_Runnable.class) { //添加同步锁
if( numTicket == 0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前 "+Thread.currentThread().getName()+" 窗口 正在出售第 "+ (numTicket--) +" 号票中...");
}
}
}
}
h.
死锁
概述
* 多线程同步的时候,如果同步代码嵌套,使用相同的锁,就会出现死锁现象
* 所以 同步代码块 尽量不要嵌套使用
代码
public class Demo05_多线程_死锁问题 {
private static String s1 ="筷子左"; // 定义筷子 来区分两把不同的 锁
private static String s2 ="筷子右"; // 定义筷子 来区分两把不同的 锁
public static void main(String[] args) {
new Thread("线程一 "){
public void run() {
while (true) {
synchronized (s1) {
System.out.println(this.getName()+" 拿到 "+s1+" 等待 "+s2);
synchronized (s2) { // 同步代码块的嵌套 会出现死锁现象
System.out.println(this.getName()+" 拿到 "+s2+" 开吃");
}
}
}
};
}.start();
new Thread("线程二 "){
public void run() {
while (true) {
synchronized (s2) {
System.out.println(this.getName()+" 拿到 "+s2+" 等待 "+s1);
synchronized (s1) { // 同步代码块的嵌套 会出现死锁现象
System.out.println(this.getName()+" 拿到 "+s1+" 开吃");
}
}
}
};
}.start();
// 运行结果:代码执行到一小半就不执行了,但是程序还没有停止(控制台
// 还运行着红方框),所以这个时候程序 就出现了 死锁状态,所以
// 我们在使用同步锁的时候,避免嵌套使用。
}
}
i.
回顾以前说过的线程安全问题
* 看源码:
* Vector,StringBuffer,Hashtable,Collections.synchroinzed(xxx)
* 查看源码可知,这些类中的某些方法 只要加上 synchroinzed 就证明该类是线程安全的,同步的。
* 具体的类
* Vector是线程安全的 --jdk1.0
* 格式:
* public synchronized void addElement(E obj) {}
* 是线程安全,同步,因为该类中 addElement(E obj)方法加了 synchroinzed,所以该方法是非静态的 同步方法
* ArrayList是线程不安全的 --jdk1.2
* 格式:
* public boolean add(E e) {}
* 是线程不安全,不同步。因为该类中的 add(E e)方法 没有加 synchroinzed
* StringBuffer是线程安全的 --jdk1.0
* 格式:
* public synchronized StringBuffer append(Object obj) {}
* 是线程安全,同步。因为该类中的 append(Object obj) 加了 synchroinzed,所以该方法是非静态的 同步方法
* StringBuilder是线程不安全的 --jdk1.5
* 格式:
* public StringBuilder append(Object obj){}
* 是线程不安全,不同步。 因为该类中的 append(Object obj) 没有加 synchroinzed
* Hashtable是线程安全的 --jdk1.0
* 格式:
* public synchronized V put(K key, V value){}
* 是线程安全的,同步。因为该类的put方法中加入了 synchroinzed,所以该方法是非静态的 同步方法
* HashMap是线程不安全的 --jdk1.2
* public V put(K key, V value) {}
* 是线程不安全的,不同步。 因为该类中的 put(K key, V value) 没有加 synchroinzed
Collections.synchroinzedXXX(xxx) 可以将线程不安全的设置为线程安全的(同步的)。
多线程 【面试题】
1. 什么是多线程的并发和并行?
并行:两个任务同时执行,在执行A任务的同时,也执行B任务。前提是:这需要多核CPU
并发:两个任务同时请求执行,但cpu只能执行一个,所以只能交替执行,由于交替执行的时间间隔太快,所以给人看起来是同时执行,但其实不是。
2. JVM的启动是多线程的吗?
jvm的启动是多线程的,至少启动了 垃圾回收线程 和 主线程
3. Java程序的运行原理
Java程序的启动,会先启动jvm虚拟机,再由jvm虚拟机去启动主线程,再由主线程去调用类的main函数
4. 多线程的实现之:实现Runnable接口的原理
其实是通过多态来实现的。
* 原理:
* 其实在Thread类里边有一个成员变量target,用来记录传过来的 Runnable接口的子类对象,
将传过来的对象,赋值给target,这样调用 Thread 类中的run()方法的时候,根据多态的原理,
* 编译看左边,运行看右边。实际调用的还是MyRunnable类的run()方法。
* 源码
class Thread {
private Runnable target;
public Thread(Runnable target) { //Runnable target = new MyRunnable();
this.target = target;
}
public void run() {
if (target != null) {
target.run();
}
}
}
5. 多线程的两种实现方式的区别:
继承 Thread类
* 代码简单,能够直接使用Thread中的方法
* 好处:因为是继承Thread类,所以可以直接使用Thread中的公共方法
* 弊端:如果类已经有了继承的其他类,就不能使用这个方法
实现 Runnable接口
* 代码功能扩展性强
* 好处:即使该类继承了其他类,也能使用这种方法,因为类是单继承,多实现的
* 弊端:不能直接使用Thread中的公共方法,所以必须要先获取到Thread类的对象后,才能使用到Thread中的方法,代码复杂
6. 线程默认优先级分为几个级别?
* 默认 5
* 范围:1 ~ 10
7. 线程的优先级别高,就代表该线程就会第一个执行吗?
* 不一定
* 线程的优先级越高代码该线程在一定程度上可以获取到更多的执行权,但不能完全代表该线程就会第一个执行
8. 同步代码块 和同步方法的锁对象分别是是什么?
非静态的同步代码块的锁对象是:this
静态的同步代码块的锁对象的是:该类的字节码对象 --类名.class
阅读全文
0 0
- Day24 --多线程(上)
- day24(多线程)
- day24<多线程>
- 多线程+JAVA学习笔记-DAY24
- 集合框架(day24)
- day24
- day24
- day24
- day24
- Day24
- day24
- Java-----多线程(上)
- Java多线程(上)
- Java 多线程(上)
- Java_多线程(上)
- 多线程开发(上)
- java多线程(上)
- 多线程(上)
- Day22 --序列流 内存输出流 随机访问流 对象操作流 数据输入输出流 打印流 标准输入输出流 Properties
- posix线程的误区: 线程是否启动
- Day23 --递归
- 第三章 ALDS1_1_A:Insertion Sort 插入排序法
- 事件分发和NestedScrolling(一)
- Day24 --多线程(上)
- 基本最小生成树—Kruskal
- Day25 --多线程(下) 设计模式 GUI
- 一篇文章彻底搞懂java动态代理的实现
- 数字与模拟通信系统第四章——带通信号
- JSON 数据交换
- TeraTerm Language 帮助文档2-[数据类型]
- Day26 --网络编程
- 【R语言】数据操作