第十九章 多线程
1. 多线程基本概述
1.1 什么是进程?什么是线程?
- 进程是一个应用程序(1个进程是一个软件),线程是一个进程中的执行场景/执行单元;一个进程可以启动多个线程。- 对于java程序来说,当在DOS命令窗口中输入:java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾;最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。- 进程可以看做是现实生活当中的公司,线程可以看做是公司当中的某个员工。- java中之所以有多线程机制,目的就是为了提高程序的处理效率。
1.2 线程、进程在内存中的表现

线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
- 使用了多线程机制之后,main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。- 对于多核的CPU电脑来说,真正的多线程并发是没问题的;对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情同时在做;电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,人类的眼睛产生了错觉,感觉是动画的。
2. 实现线程的方式
2.1 第一种方式
- 编写一个类,直接继承java.lang.Thread,重写run方法。
public class ThreadTest02 {public static void main(String[] args) {// 这里是main方法,这里的代码属于主线程,在主栈中运行。// 新建一个分支线程对象MyThread t = new MyThread();// 启动线程//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)// start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。// 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。// 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。// run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。t.start();// 这里的代码还是运行在主线程中。for(int i = 0; i < 1000; i++){System.out.println("主线程--->" + i);}}}class MyThread extends Thread {@Overridepublic void run() {// 编写程序,这段程序运行在分支线程中(分支栈)。for(int i = 0; i < 1000; i++){System.out.println("分支线程--->" + i);}}}
2.1.1 线程的start方法

2.2 第二种方式
- 编写一个类,实现java.lang.Runnable接口,实现run方法。
public class ThreadTest03 {public static void main(String[] args) {// 创建一个可运行的对象//MyRunnable r = new MyRunnable();// 将可运行的对象封装成一个线程对象//Thread t = new Thread(r);Thread t = new Thread(new MyRunnable()); // 合并代码// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("主线程--->" + i);}}}// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。class MyRunnable implements Runnable {@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("分支线程--->" + i);}}}
2.2.1 匿名内部类方式
public class ThreadTest04 {public static void main(String[] args) {// 创建线程对象,采用匿名内部类方式。// 这是通过一个没有名字的类,new出来的对象。Thread t = new Thread(new Runnable(){@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("t线程---> " + i);}}});// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("main线程---> " + i);}}}
注意: 第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。
3. 线程对象的生命周期

运行状态进入阻塞状态,还有join方法
4. 获取当前线程对象
1、怎么获取当前线程对象?
Thread t = Thread.currentThread(); 返回值t就是当前线程。
2、获取线程对象的名字
String name = 线程对象.getName();
3、修改线程对象的名字
线程对象.setName(“线程名字”);
4、当线程没有设置名字的时候,默认的名字有什么规律?(了解一下)
主线程默认:main
Thread-0
Thread-1
Thread-2
…..
public class ThreadTest05 {public void doSome(){// 这样就不行了//this.getName();//super.getName();// 但是这样可以String name = Thread.currentThread().getName();System.out.println("------->" + name);}public static void main(String[] args) {ThreadTest05 tt = new ThreadTest05();tt.doSome();//currentThread就是当前线程对象// 这个代码出现在main方法当中,所以当前线程就是主线程。Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName()); //main// 创建线程对象MyThread2 t = new MyThread2();// 设置线程的名字t.setName("t1");// 获取线程的名字String tName = t.getName();System.out.println(tName); //Thread-0MyThread2 t2 = new MyThread2();t2.setName("t2");System.out.println(t2.getName()); //Thread-1\t2.start();// 启动线程t.start();}}class MyThread2 extends Thread {public void run(){for(int i = 0; i < 100; i++){// currentThread就是当前线程对象。当前线程是谁呢?// 当t1线程执行run方法,那么这个当前线程就是t1// 当t2线程执行run方法,那么这个当前线程就是t2Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName() + "-->" + i);//System.out.println(super.getName() + "-->" + i);//System.out.println(this.getName() + "-->" + i);}}}
5. 关于线程的sleep方法
/*关于线程的sleep方法:static void sleep(long millis)1、静态方法:Thread.sleep(1000);2、参数是毫秒3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。这行代码出现在A线程中,A线程就会进入休眠。这行代码出现在B线程中,B线程就会进入休眠。4、Thread.sleep()方法,可以做到这种效果:间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。*/public class ThreadTest06 {public static void main(String[] args) {// 让当前线程进入休眠,睡眠5秒// 当前线程是主线程!!!/*try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}*/// 5秒之后执行这里的代码//System.out.println("hello world!");for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);// 睡眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
5.1 Thread.sleep()方法的一个面试题
public class ThreadTest07 {public static void main(String[] args) {// 创建线程对象Thread t = new MyThread3();t.setName("t");t.start();// 调用sleep方法try {// 问题:这行代码会让线程t进入休眠状态吗?t.sleep(1000 * 5); // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);// 这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。// 这样代码出现在main方法中,main线程睡眠。} catch (InterruptedException e) {e.printStackTrace();}// 5秒之后这里才会执行。System.out.println("hello World!");}}class MyThread3 extends Thread {public void run(){for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}}
6. 终止线程的睡眠
/*sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程??注意:这个不是终断线程的执行,是终止线程的睡眠。*/public class ThreadTest08 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable2());t.setName("t");t.start();// 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)t.interrupt(); // 干扰,一盆冷水过去!}}class MyRunnable2 implements Runnable {// 重点:run()当中的异常不能throws,只能try catch// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "---> begin");try {// 睡眠1年Thread.sleep(1000 * 60 * 60 * 24 * 365);} catch (InterruptedException e) {// 打印异常信息//e.printStackTrace();}//1年之后才会执行这里System.out.println(Thread.currentThread().getName() + "---> end");// 调用doOther//doOther();}// 其它方法可以throws/*public void doOther() throws Exception{}*/}
6.1 强行终止一个线程的执行
/*在java中怎么强行终止一个线程的执行。这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。*/public class ThreadTest09 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable3());t.setName("t");t.start();// 模拟5秒try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 5秒之后强行终止t线程t.stop(); // 已过时(不建议使用。)}}class MyRunnable3 implements Runnable {@Overridepublic void run() {for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
6.2 合理的终止一个线程的执行
public class ThreadTest10 {public static void main(String[] args) {MyRunable4 r = new MyRunable4();Thread t = new Thread(r);t.setName("t");t.start();// 模拟5秒try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}// 终止线程// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。r.run = false;}}class MyRunable4 implements Runnable {// 打一个布尔标记boolean run = true;@Overridepublic void run() {for (int i = 0; i < 10; i++){if(run){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}else{// return就结束了,你在结束之前还有什么没保存的。// 在这里可以保存呀。//save....//终止当前线程return;}}}}
注意: if判断语句是在for循环里
7. 线程的调度
7.1 常见的线程调度模型
- 抢占式调度模型:
那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型。
- 均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样,平均分配,一切平等,有一些编程语言,线程调度模型采用的是这种方式。
7.2 java中提供了哪些方法是和线程调度有关系的
实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
最低优先级1,默认优先级是5,最高优先级10,优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程,yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用;yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意: 在回到就绪之后,有可能还会再次抢到。
实例方法:
void join() 合并线程
class MyThread1 extends Thread {public void doSome(){MyThread2 t = new MyThread2();t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。}}class MyThread2 extends Thread{}
8. 线程的优先级
public class ThreadTest11 {public static void main(String[] args) {// 设置主线程的优先级为1Thread.currentThread().setPriority(1);/*System.out.println("最高优先级" + Thread.MAX_PRIORITY);System.out.println("最低优先级" + Thread.MIN_PRIORITY);System.out.println("默认优先级" + Thread.NORM_PRIORITY);*/// 获取当前线程对象,获取当前线程的优先级Thread currentThread = Thread.currentThread();// main线程的默认优先级是:5//System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());Thread t = new Thread(new MyRunnable5());t.setPriority(10);t.setName("t");t.start();// 优先级较高的,只是抢到的CPU时间片相对多一些。// 大概率方向更偏向于优先级比较高的。for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "-->" + i);}}}class MyRunnable5 implements Runnable {@Overridepublic void run() {// 获取线程优先级//System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "-->" + i);}}}
8.1 yield方法
/*让位,当前线程暂停,回到就绪状态,让给其它线程。静态方法:Thread.yield();*/public class ThreadTest12 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable6());t.setName("t");t.start();for(int i = 1; i <= 10000; i++) {System.out.println(Thread.currentThread().getName() + "--->" + i);}}}class MyRunnable6 implements Runnable {@Overridepublic void run() {for(int i = 1; i <= 10000; i++) {//每100个让位一次。if(i % 100 == 0){Thread.yield(); // 当前线程暂停一下,让给主线程。}System.out.println(Thread.currentThread().getName() + "--->" + i);}}}
8.2 线程合并
public class ThreadTest13 {public static void main(String[] args) {System.out.println("main begin");Thread t = new Thread(new MyRunnable7());t.setName("t");t.start();//合并线程try {t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main over");}}class MyRunnable7 implements Runnable {@Overridepublic void run() {for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}}
9. 关于多线程并发环境下,数据的安全问题
- 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
- 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。
9.1 什么时候数据在多线程并发的环境下会存在安全问题
9.1.1 三个条件
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为
9.1.2 怎么解决线程安全问题
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)用排队执行解决线程安全问题,这种机制被称为:线程同步机制,专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是多线程并发,效率较高(异步就是并发)
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型;线程排队执行,效率较低(同步就是排队)
/*银行账户使用线程同步机制,解决线程安全问题。*/public class Account {// 账号private String actno;// 余额private double balance; //实例变量。//对象Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法public void withdraw(double money){//int i = 100;//i = 101;// 以下这几行代码必须是线程排队的,不能并发。// 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。/*线程同步机制的语法是:synchronized(){// 线程同步代码块。}synchronized后面小括号中传的这个“数据”是相当关键的。这个数据必须是多线程共享的数据。才能达到多线程排队。()中写什么?那要看你想让哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。这里的共享对象是:账户对象。账户对象是共享的,那么this就是账户对象吧!!!不一定是this,这里只要是多线程共享的那个对象就行。在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)100个对象,100把锁。1个对象1把锁。以下代码的执行原理?1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。*///Object obj2 = new Object();//synchronized (this){//synchronized (obj) {//synchronized ("abc") { // "abc"在字符串常量池当中。//synchronized (null) { // 报错:空指针。//synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。double before = this.getBalance();double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);//}}}public class AccountThread extends Thread {// 两个线程必须共享同一个账户对象。private Account act;// 通过构造方法传递过来账户对象public AccountThread(Account act) {this.act = act;}public void run(){// run方法的执行表示取款操作。// 假设取款5000double money = 5000;// 取款// 多线程并发执行这个方法。//synchronized (this) { //这里的this是AccountThread对象,这个对象不共享!synchronized (act) { // 这种方式也可以,只不过扩大了同步的范围,效率更低了。act.withdraw(money);}System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());}}public class Test {public static void main(String[] args) {// 创建账户对象(只创建1个)Account act = new Account("act-001", 10000);// 创建两个线程Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);// 设置namet1.setName("t1");t2.setName("t2");// 启动线程取款t1.start();t2.start();}}
9.1.3 那些变量存在线程安全问题
实例变量:在堆中
静态变量:在方法区
局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题
局部变量不共享,(一个线程一个栈)局部变量在栈中,所以局部变量永远都不会共享
实例变量在堆中,堆只有1个;静态变量在方法区中,方法区只有1个,堆和方法区都是多线程共享的,所以可能存在线程安全问题
局部变量+常量(不可变):不会有线程安全问题;成员变量:可能会有线程安全问题
如果使用局部变量的话:建议使用:StringBuilder;因为局部变量不存在线程安全问题,选择StringBuilder,StringBuffer效率比较低; ArrayList是非线程安全的;Vector是线程安全的; HashMap HashSet是非线程安全的;Hashtable是线程安全的
9.1.4 扩大同步范围
- synchronized出现在实例方法中
public class Account {// 账号private String actno;// 余额private double balance;public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法/*在实例方法上可以使用synchronized吗?可以的。synchronized出现在实例方法上,一定锁的是this,只能是this。不能是其他的对象了,所以这种方式不灵活。另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。synchronized使用在实例方法上有什么优点? 代码写的少了,简洁如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。*/public synchronized void withdraw(double money){double before = this.getBalance(); // 10000double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}}public class AccountThread extends Thread {// 两个线程必须共享同一个账户对象。private Account act;// 通过构造方法传递过来账户对象public AccountThread(Account act) {this.act = act;}public void run(){// run方法的执行表示取款操作。// 假设取款5000double money = 5000;// 取款// 多线程并发执行这个方法。act.withdraw(money);System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());}}public class Test {public static void main(String[] args) {// 创建账户对象(只创建1个)Account act = new Account("act-001", 10000);// 创建两个线程Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);// 设置namet1.setName("t1");t2.setName("t2");// 启动线程取款t1.start();t2.start();}}
10. synchronized
10.1 synchronized的三种写法
第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized<br />表示共享对象一定是this,并且同步代码块是整个方法体。<br /> <br /> 第三种:在静态方法上使用synchronized<br />表示找类锁。类锁永远只有1把,就算创建了100个对象,那类锁也只有一把;对象锁:1个对象1把锁,100个对象100把锁,类锁:100个对象,也可能只是1把类锁。<br />
10.2 面试题
10.2.1
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//不需要,因为doOther()方法没有synchronizedpublic class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}}class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}}
10.2.2
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//需要public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}}class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}}
10.2.3
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//不需要,因为MyClass对象是两个,两把锁。public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new MyThread(mc1);Thread t2 = new MyThread(mc2);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}}class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}}
10.2.4
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new MyThread(mc1);Thread t2 = new MyThread(mc2);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}}class MyClass {// synchronized出现在静态方法上是找类锁。public synchronized static void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized static void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}}
10.3 死锁问题

10.3.1 死锁代码实现
public class DeadLock {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();// t1和t2两个线程共享o1,o2Thread t1 = new MyThread1(o1,o2);Thread t2 = new MyThread2(o1,o2);t1.start();t2.start();}}class MyThread1 extends Thread{Object o1;Object o2;public MyThread1(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){}}}}class MyThread2 extends Thread {Object o1;Object o2;public MyThread2(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}}
11. 总结:开发中应该怎么解决线程安全问题
是一上来就选择线程同步(synchronized)吗?
不是,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,用户体验差;在不得已的情况下再选择线程同步机制
第一种方案:
尽量使用局部变量代替“实例变量和静态变量”
第二种方案:
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择线程同步机制(synchronized)了
12. 守护线程
- java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
- 守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
注意:主线程main方法是一个用户线程。
- 守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程,一直在那里看着,每到00:00的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了
public class ThreadTest14 {public static void main(String[] args) {Thread t = new BakDataThread();t.setName("备份数据的线程");// 启动线程之前,将线程设置为守护线程t.setDaemon(true);t.start();// 主线程:主线程是用户线程for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}class BakDataThread extends Thread {public void run(){int i = 0;// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。while(true){System.out.println(Thread.currentThread().getName() + "--->" + (++i));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
13. 定时器
- 定时器的作用:
间隔特定的时间,执行特定的程序。例如:每周要进行银行账户的总账操作、每天要进行数据的备份操作、在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见
- 那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
import java.text.SimpleDateFormat;import java.util.Date;import java.util.Timer;import java.util.TimerTask;/*使用定时器指定定时任务。*/public class TimerTest {public static void main(String[] args) throws Exception {// 创建定时器对象Timer timer = new Timer();//Timer timer = new Timer(true); //守护线程的方式// 指定定时任务//timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date firstTime = sdf.parse("2020-03-14 09:34:30");//timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);// 每年执行一次。//timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);//匿名内部类方式timer.schedule(new TimerTask(){@Overridepublic void run() {// code....}} , firstTime, 1000 * 10);}}// 编写一个定时任务类// 假设这是一个记录日志的定时任务class LogTimerTask extends TimerTask {@Overridepublic void run() {// 编写你需要执行的任务就行了。SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String strTime = sdf.format(new Date());System.out.println(strTime + ":成功完成了一次数据备份!");}}
14. 实现线程的第三种方式
- 实现Callable接口。(JDK8新特性。)这种方式实现的线程可以获取线程的返回值,之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。 ```java import java.util.concurrent.Callable; // JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。 import java.util.concurrent.FutureTask;
/ 实现线程的第三种方式: 实现Callable接口 这种方式的优点:可以获取到线程的执行结果。 这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。 / public class ThreadTest15 { public static void main(String[] args) throws Exception {
// 第一步:创建一个“未来任务类”对象。// 参数非常重要,需要给一个Callable接口实现类对象。FutureTask task = new FutureTask(new Callable() {// call()方法就相当于run方法。只不过这个有返回值@Overridepublic Object call() throws Exception {// 线程执行一个任务,执行之后可能会有一个执行结果// 模拟执行System.out.println("call method begin");Thread.sleep(1000 * 10);System.out.println("call method end!");int a = 100;int b = 200;return a + b; //自动装箱(300结果变成Integer)}});// 创建线程对象Thread t = new Thread(task);// 启动线程t.start();// 这里是main方法,这是在主线程中。// 在主线程中,怎么获取t线程的返回结果?// get()方法的执行会导致“当前线程阻塞”Object obj = task.get();System.out.println("线程执行结果:" + obj);// main方法这里的程序要想执行必须等待get()方法的结束// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果// 另一个线程执行是需要时间的。System.out.println("hello world!");}
}
<a name="3U1fk"></a>## 15. Object类中的wait和notify方法- wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是 Object类中自带的;wait方法和notify方法不是通过线程对象调用- wait()方法作用?Object o = new Object();<br />o.wait();<br />表示: 让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止;o.wait()方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态- notify()方法作用?Object o = new Object();<br />o.notify();<br />表示:唤醒正在o对象上等待的线程。<br />- notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程<br /><a name="fF5WW"></a>### 15.1 生产者和消费者模式> notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放<a name="7spZX"></a>### 15.2 生产者和消费者模式代码实现```javaimport java.util.ArrayList;import java.util.List;/*1、使用wait方法和notify方法实现“生产者和消费者模式”2、什么是“生产者和消费者模式”?生产线程负责生产,消费线程负责消费。生产线程和消费线程要达到均衡。这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。4、wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。7、模拟这样一个需求:仓库我们采用List集合。List集合中假设只能存储1个元素。1个元素就表示仓库满了。如果List集合中元素个数是0,就表示仓库空了。保证List集合中永远都是最多存储1个元素。必须做到这种效果:生产1个消费1个。*/public class ThreadTest16 {public static void main(String[] args) {// 创建1个仓库对象,共享的。List list = new ArrayList();// 创建两个线程对象// 生产者线程Thread t1 = new Thread(new Producer(list));// 消费者线程Thread t2 = new Thread(new Consumer(list));t1.setName("生产者线程");t2.setName("消费者线程");t1.start();t2.start();}}// 生产线程class Producer implements Runnable {// 仓库private List list;public Producer(List list) {this.list = list;}@Overridepublic void run() {// 一直生产(使用死循环来模拟一直生产)while(true){// 给仓库对象list加锁。synchronized (list){if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。try {// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒消费者进行消费list.notifyAll();}}}}// 消费线程class Consumer implements Runnable {// 仓库private List list;public Consumer(List list) {this.list = list;}@Overridepublic void run() {// 一直消费while(true){synchronized (list) {if(list.size() == 0){try {// 仓库已经空了。// 消费者线程等待,释放掉list集合的锁list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到此处说明仓库中有数据,进行消费。Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒生产者生产。list.notifyAll();}}}}
