多线程
概述
进程是程序执行的最小单位,线程是进程的一部分,即一个进程对应多个线程,一个进程也可以启动多个线程。对于Java程序来说,当在DOS命令窗口输入:java HelloWorld 回车之后,会先启动JVM进程,JVM再启动一个主线程调用main()方法,同时再启动一个GC线程回收垃圾。因此整个操作是多线程。使用多线程之后,main方法结束了,JVM也不一定结束。详见下图:
JVM内存模型中,堆和方法区是多线程共享的,栈是各线程相互独立的。为什么栈是私有的?因为一个栈对应一个线程,线程之间执行各自任务,互不干扰,多线程并发是为了提高系统效率。
单核CPU能做到真正的多线程并发吗?不能。但是给人的感觉是能,就像电影院胶卷播放电影。
分析以下代码,总共指向了几个线程?(除了GC)
package thread;public class ThreadTest01 {public static void main(String[] args) {System.out.println("main begin...");m1();System.out.println("main finish...");}private static void m1() {System.out.println("m1 begin...");m2();System.out.println("m1 finish...");}private static void m2() {System.out.println("m2 begin...");m3();System.out.println("m2 finish...");}private static void m3() {System.out.println("m3...");}}输出:main begin...m1 begin...m2 begin...m3...m2 finish...m1 finish...main finish...//事实上只有一个,即main线程。以上方法都是在main线程中完成的。
线程实现的几种方式★★
方式一:继承Thread类,并重写run方法
package com.simon;/*** 实现多线程的方式一:继承Thread类,并重写run方法*/public class ThreadAcheive01 {public static void main(String[] args) {//这是一个main方法,这里的代码属于主线程,在主栈中执行。//新建一个分支线程的对象MyThread mt = new MyThread();//启动线程/*** start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,* 这段代码任务完成之后,瞬间就结束了,这段代码的任务只是为了开辟一个新的栈空间,* 只要新的栈空间开出来,start()方法就结束了,线程就启动成功了。* 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)* run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。* 如果将mt.run()代替mt.start(),系统是不会启动线程的,也不会分配新的分支栈,* 不能并发,本质上是单线程.方法体中代码执行顺序,总是自上而下的。* 是先启动分支栈,然后主线程再执行循环语句*/mt.start();for (int i = 0; i < 10; i++) {System.out.println("主线程--->" + i);}}}class MyThread extends Thread{@Overridepublic void run() {//编写程序,这段程序运行在分支线程中(分支栈)for (int i = 0; i < 10; i++) {System.out.println("分支线程--->" + i);}}}//输出如下:// 主线程--->0//分支线程--->0//主线程--->1//分支线程--->1//主线程--->2//分支线程--->2//主线程--->3//分支线程--->3//分支线程--->4//主线程--->4//分支线程--->5//主线程--->5//分支线程--->6//分支线程--->7//分支线程--->8//分支线程--->9//主线程--->6//主线程--->7//主线程--->8//主线程--->9
一幅图来区别run和start方法
方式二:编写一个类实现Runnable接口
package thread;/*** 实现编程的第二种方式,编写一个类实现java.lang.Runnable接口*/public class ThreadAcheive02 {public static void main(String[] args) {//创建一个可运行对象MyRunnable mr = new MyRunnable();//将可运行对象封装成一个线程对象//注意,有一个Thread类的构造方法就是这样,参数是一个可运行对象Thread t = new Thread(mr);//启动线程t.start();for (int i = 0; i < 10; i++) {System.out.println("主线程--->" + i);}}}//这并不是一个线程类,而是一个可运行类,它不是一个线程class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("分支线程--->" + i);}}}//输出如下:// 主线程--->0//分支线程--->0//主线程--->1//分支线程--->1//分支线程--->2//主线程--->2//分支线程--->3//主线程--->3//分支线程--->4//主线程--->4//分支线程--->5//主线程--->5//分支线程--->6//主线程--->6//分支线程--->7//主线程--->7//分支线程--->8//主线程--->8//分支线程--->9//主线程--->9
方式二相对于方式一其实更好,在实现Runnable接口的基础上还可以继承,一句话:面向接口编程。所以,既然面向接口编程,那么可以采取匿名内部类的方式去实现:
方式二的改进:匿名内部类
package thread;/*** 方式二采用匿名内部类*/public class ThreadAcheiveTest2_Improve {public static void main(String[] args) {//接口是不能new对象的,但是这里,Runnable不再是接口,而是一个匿名内部类,所以可以newThread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("分支线程-->" + (i + 1));}}});t.start();for (int i = 0; i < 10; i++) {System.out.println("主线程-->" + (i + 1));}}}//输出如下:// 主线程-->1//主线程-->2//主线程-->3//分支线程-->1//主线程-->4//主线程-->5//主线程-->6//分支线程-->2//分支线程-->3//主线程-->7//分支线程-->4//主线程-->8//主线程-->9//分支线程-->5//主线程-->10//分支线程-->6//分支线程-->7//分支线程-->8//分支线程-->9//分支线程-->10
方式三:实现Callable接口
package thread;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;//java.util.concurrent就是我们常说的JUC,属于Java的并发包,老jdk没有这个包,是jdk的新特性/*** 实现线程的第三种方式:实现Callable接口(jdk8新特性)* 这种方式实现的线程可以获取线程的返回值。* 场景:系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,* 这种情况下最好实现Callable接口*/public class ThreadAcheiveTest03 {public static void main(String[] args) {//创建一个“未来任务类”对象。// Callable是一个接口,可用匿名内部类,call方法相当于run方法,只不过有返回值FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception {//线程执行一个任务,执行之后可能会有一个结果,以下模拟执行System.out.println("call method begin..");Thread.sleep(1000*5);System.out.println("call method ends..");int a=10,b=20;return a+b; //自动装箱(基本数据类型变为引用数据类型)}});//创建线程对象Thread t = new Thread(task);//启动线程t.start();//在main方法即主线程中,如何获得t线程的返回结果?try {//这行代码会让主线程受阻么?//main方法想要执行必须等待get方法的结束,而get方法是为了拿到另一个线程的执行结果//这一行代码肯定是在call方法执行完并且有返回值以后才执行,所以main线程要受阻Object obj = task.get();System.out.println("线程执行结果:" + obj);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}System.out.println("hello,world");}}//内容输出://call method begin..//call method ends..//线程执行结果:30//hello,world//优缺点://优点:可获得线程的执行结果//缺点:执行效率低--在获取t线程执行结果时受阻。其实优缺点都讲的返回结果的问题。
线程的生命周期
线程的生命周期包括:新建、就绪、运行、阻塞和死亡五个时期。其中,在代码中能体现的是,就绪调用start方法,运行时调用run方法,阻塞是放弃CPU执行权(这个不算),死亡是run方法执行结束。
关于线程的几个方法
package thread;/*** 三个内容:* 1、怎么获取当前线程的对象 static Thread currentThread();* 2、获取线程对象的名字 String name = 线程对象.getName();* 3、修改线程对象的名字 线程对象.setName("线程名字");* 4、当线程没有设置名字的时候,默认名字的规律是:Thread-0、Thread-1、Thread-2...*/public class ThreadTest02 {public static void main(String[] args) {System.out.println(Thread.currentThread().getName()); //mainMyThread2 mt2 = new MyThread2();//若没有设置线程名字,则默认为:Thread-0mt2.setName("设置当前线程名字为:分支线程A");String mt2Name = mt2.getName();System.out.println(mt2Name);mt2.start();for (int i = 0; i < 10; i++) {System.out.println("主线程-->" + (i + 1));}}}class MyThread2 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("分支线程--->" + (i + 1));}}}//输出内容如下://设置当前线程名字为:分支线程A//主线程-->1//主线程-->2//主线程-->3//主线程-->4//主线程-->5//主线程-->6//分支线程--->1//主线程-->7//分支线程--->2//主线程-->8//分支线程--->3//主线程-->9//分支线程--->4//主线程-->10//分支线程--->5//分支线程--->6//分支线程--->7//分支线程--->8//分支线程--->9//分支线程--->10
package thread;/*** 关于线程的sleep方法:* static void sleep(long mills)* 1、静态方法* 2、参数是毫秒* 3、作用:让当前线程进入休眠即阻塞状态,放弃CPU执行权给其他线程使用.* 4、每隔多久执行一次某些代码*/public class ThreadTest03 {//让当前线程休眠public static void main(String[] args) {try {Thread.sleep(1000*3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello,Thread sleep method...");}}
package thread;/*** 关于Thread.sleep()方法的面试题*/public class ThreadTest04 {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread4();t.setName("t");t.start();//调用sleep方法//问题:这行代码会让线程t进入休眠状态吗?//并不会,因为在执行的时候还是会转成:Thread.sleep(1000*5);//这行代码的作用是,让当前线程进入休眠,也就是说main线程进入休眠//这种感觉和下面这个Test类的情形有点像t.sleep(1000*3);//3秒之后才会输出System.out.println("hello,world");}}class MyThread4 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "-->" + (i + 1));}}}//输出://t-->1//t-->2//t-->3//t-->4//t-->5//t-->6//t-->7//t-->8//t-->9//t-->10//hello,world
package demo2;public class Test {public static void main(String[] args) {Test.doSome(); //do someTest test = new Test();test.doSome(); //do sometest = null;test.doSome(); //do some,并没有出现空指针异常}public static void doSome(){System.out.println("do some");}}
package thread;/*** sleep睡眠太久了,如果希望半道上醒来,该怎么办?* 也就是说,如何叫醒正在睡眠的线程?*/public class ThreadTest05 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable2());t.setName("t");t.start();//希望5秒之后醒来try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}//一盆冷水泼过来//本质上是让MyRunnable2的run方法的try语句块出现异常,异常机制t.interrupt();}}class MyRunnable2 implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "--->begin");try {//这里sleep为什么只能try catch...?因为run方法在父类中没有抛出任何异常,子类异常不能更多Thread.sleep(1000*60*60*24*365);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "--->end");}}//输出内容如下://t--->begin//java.lang.InterruptedException: sleep interrupted// at java.lang.Thread.sleep(Native Method)// at thread.MyRunnable2.run(ThreadTest05.java:31)// at java.lang.Thread.run(Thread.java:748)//t--->end
package thread;/*** 在Java中如何强行终止一个线程的执行?* 缺点是什么??容易损坏数据*/public class ThreadTest06 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable3());t.setName("t");t.start();try {Thread.sleep(1000*5);} catch (InterruptedException e) {e.printStackTrace();}t.stop();}}class MyRunnable3 implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "-->"+ (i+1));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}//输出如下://t-->1//t-->2//t-->3//t-->4//t-->5
package com.simon;/*** 如何合理终止一个线程的执行? 改标志位!*/public class ThreadTest07 {public static void main(String[] args) {MyRunnable4 mr = new MyRunnable4();Thread t = new Thread(mr);t.setName("t");t.start();try {Thread.sleep(1000*3);} catch (InterruptedException e) {e.printStackTrace();}mr.run =false;}}class MyRunnable4 implements Runnable{boolean run = true;@Overridepublic void run() {for (int i = 0; i < 10; i++) {if (run){System.out.println(Thread.currentThread().getName() + "-->"+ (i+1));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}else {//在结束之前有什么善后工作要做的,都在return之前做完,就能保证数据不丢失return;}}}}//输出结果如下://t-->1//t-->2//t-->3
package mythread;public class ThreadTest02 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();for (int i = 0; i < 10000; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10000; i++) {if (i%1000==0){//每隔1000,即当999-->1000,3999-->4000时,两者都不会连续而是硬生生让给了main线程Thread.yield();}System.out.println(Thread.currentThread().getName() + ":" + i);}}}

这就是线程礼让,让一点,程度轻微。
package mythread;/*** 线程合并:这里,t.join()是指t线程加入进来之后,* main线程等待t线程结束后再执行,让整个线程,程度较深。*/public class ThreadTest03 {public static void main(String[] args) {System.out.println("main begin...");Thread t = new Thread(new MyRunnable2());t.start();try {t.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main over...");}}class MyRunnable2 implements Runnable{@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + ":" + (i + 1));}}}//内容输出为://main begin...//Thread-0:1//Thread-0:2//Thread-0:3//Thread-0:4//Thread-0:5//main over...
线程安全
假定有以下情况,两个人都去ATM上输入相同账号和密码交易,同时进行取款的动作,并且取完。由于网络存在延迟,总有一个人先取完钱,如果银行存储的数据未能及时变更,那么第二个人应该也能继续取钱,这就意味着,由于数据不同步导致的数据安全会给日常生活带来极大的麻烦。所以,数据安全是数据交易的生命,如果谈不上安全,那么就没必要进行数据交易了。由此可知,什么情况下数据不安全?①多线程并发 ②有共享的数据 ③数据有修改行为 解决办法是让线程排队执行,宁可牺牲效率,也要保证数据同步。
这里,存在同步和异步的概念,同步就是排队、相互等待,异步就是并发,谁也不用等谁。
具体解决线程并发的问题:synchronized,参数很关键:此参数为多线程共享对象 。每个Java对象都有1把锁,这个锁就是标记。注意:这个共享对象一定要选好,一定是你需要排队执行的这些线程所共享的对象。如何体现在线程周期上? 运行状态—->锁池
一个账户类的并发安全事件★★
package mythread;public class Account {private String no;private double balance;Object obj = new Object();public String getNo() {return no;}public void setNo(String no) {this.no = no;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}public Account(String no, double balance) {this.no = no;this.balance = balance;}public Account() {}//取款方法public void withdraw(double money){//synchronized()中的小括号的数据必须是共享的,才能达到多线程排队的效果//()写什么?哪些对象需要排队,那么小括号就写哪些对象的共享数据。// synchronized (this){//不一定是this,只要是共享的就行,但this最佳Object obj2 = new Object();// synchronized (obj){//可以,因为是实例变量,共享// synchronized (obj2){//不可以,因为是局部变量起不到排队即同步的效果synchronized ("1"){//可以,因为常量池只有一份,共享//取款之前:double before = this.getBalance();//取款之后:double after = balance-money;try {//模拟网络延迟,此时一定会出问题Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//更新余额this.setBalance(after);}}}
package mythread;/*** 模拟2个线程对同一个账户做取款操作*/public class AccountThread extends Thread{//保证2个线程共享同一个账户对象private Account act;public AccountThread(Account act) {this.act = act;}@Overridepublic void run() {//run方法的执行表示取款操作,这里假设取款5000double money = 5000;//多线程并发执行act.withdraw(money);System.out.println(act.getNo() +"取款成功,余额为:"+ act.getBalance());}}
package mythread;public class AccountThreadTest {public static void main(String[] args) {Account act = new Account("act-001", 10000.0);Thread at1 = new AccountThread(act);Thread at2 = new AccountThread(act);at1.start();at2.start();}}
安全事件的解决方式
如何解决以上的问题呢?用同步代码块synchronized去修饰容易出现并发的那些代码块
修改withdraw()方法的代码:
//取款方法public void withdraw(double money){//synchronized()中的小括号的数据必须是共享的,才能达到多线程排队的效果//()写什么?哪些对象需要排队,那么小括号就写哪些对象的共享数据。synchronized (this){//不一定是this,只要是共享的就行//取款之前:double before = this.getBalance();//取款之后:double after = balance-money;//模拟网络延迟,此时一定会出问题try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//更新余额this.setBalance(after);}}//输出://act-001取款成功,余额为:5000.0//act-001取款成功,余额为:0.0

however,synchronized也可出现在实例方法上:
//取款方法/*synchronized出现在实例方法上,一定是锁的this,所以这种方式不灵活缺点之二是锁的范围是整个代码块,导致整个程序的执行效率降低,这种方式不常用*/public synchronized void withdraw(double money){//取款之前:double before = this.getBalance();//取款之后:double after = balance-money;//模拟网络延迟,此时一定会出问题try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//更新余额this.setBalance(after);}
哪些变量有线程安全的问题?
原则:越私有,线程越安全。所以,局部变量线程安全,实例变量在堆中,静态变量在方法区中,堆和方法区都是共享的,所以实例变量和静态变量是不安全的。使用局部变量的话,建议使用StringBuilder,因为StringBuffer效率比较低。问题:为啥是变量?锁的不应该是方法或者代码块吗?
Synchronized的三种用法:
synchronized的三种写法: ①同步代码块 ②实例方法上用synchronized,一个对象一把对象锁 ③静态方法上用synchronized,就算对象创建100个,类锁永远只有1把
面试题
—四种变形:
package thread.exam;/*** 面试题:doOther的执行是否需要doSome方法的结束?*/public class Exam1 {public static void main(String[] args) {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();try {//睡眠的作用是为了保证t1先执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread{private MyClass mc;MyThread(MyClass mc){this.mc = mc;}@Overridepublic 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*5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over..");}public void doOther(){System.out.println("doOther begin...");System.out.println("doOther over...");}}//执行结果://doSome begin..//doOther begin...//doOther over...//doSome over..
package thread.exam2;/*** 面试题:doOther的执行是否需要doSome方法的结束?*/public class Exam2 {public static void main(String[] args) {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();try {//睡眠的作用是为了保证t1先执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread{private MyClass mc;MyThread(MyClass mc){this.mc = mc;}@Overridepublic 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*5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over..");}public synchronized void doOther(){System.out.println("doOther begin...");System.out.println("doOther over...");}}//执行结果://doSome begin..//doSome over..//doOther begin...//doOther over...
package thread.exam3;/*** 面试题:doOther的执行是否需要doSome方法的结束?*/public class Exam3 {public static void main(String[] args) {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();try {//睡眠的作用是为了保证t1先执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread{private MyClass mc;MyThread(MyClass mc){this.mc = mc;}@Overridepublic 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*5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over..");}public synchronized void doOther(){System.out.println("doOther begin...");System.out.println("doOther over...");}}//执行结果://doSome begin..//doOther begin...//doOther over...//doSome over..//对象锁有2把锁,各自执行各自的
package thread.exam4;/*** 面试题:doOther的执行是否需要doSome方法的结束?*/public class Exam4 {public static void main(String[] args) {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();try {//睡眠的作用是为了保证t1先执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread{private MyClass mc;MyThread(MyClass mc){this.mc = mc;}@Overridepublic void run() {if (Thread.currentThread().getName().equals("t1")){mc.doSome();}if (Thread.currentThread().getName().equals("t2")){mc.doOther();}}}class MyClass{public synchronized static void doSome(){System.out.println("doSome begin..");try {Thread.sleep(1000*5);} 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...");}}//执行结果://doSome begin..//doSome over..//doOther begin...//doOther over...//类锁只有1把,需要等待
手写一个死锁★★
package thread.deadlock;/*** 死锁代码要求会写* 一般面试官会要求手写,只有会写的,才会在开发中注意这事,因为死锁很难调试*/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;}@Overridepublic 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;}@Overridepublic void run() {synchronized (o2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}}
日常开发如何解决线程安全的问题?
尽量不要首选synchronized,因为synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低会导致用户体验差,在不得已的情况下再选择线程同步机制。
第一种方案,尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全的问题了)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。
守护线程(后台线程)
Java语言中线程分为2大类:用户线程和守护线程。守护线程又叫后台线程,一个典型的守护线程就是垃圾回收线程。一般一个守护线程是一个死循环,所有的用户线程一旦结束,守护线程就自动结束。(注:主线程main方法是一个用户线程)守护线程的用处:比如每天0:00的时候需要数据自动备份,这个需要使用到定时器,并且我们可以将定时器设置为守护线程,一直在那看着,每到0:00就备份一次,所有的用户线程如果结束了,守护线程就自动退出,没有必要再进行数据备份了。
package thread;public class ThreadTest08 {public static void main(String[] args) {BackDateThread t = new BackDateThread();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 BackDateThread extends Thread{@Overridepublic void run() {int i = 0;//即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程也会自动终止while (true){System.out.println(Thread.currentThread().getName() + "--->" + (++i));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
定时器★★
定时器是每隔特定时间,执行特定的程序。
在Java中其实可以采用多种方式实现:可以使用sleep方法,但比较low;也可以使用java.util.Timer,可以直接用,但现在很多高级框架都是支持定时任务的,所以这种方式使用不频繁。目前实际开发中,使用较多的是Spring框架提供的SpringTask,这个框架只要进行简单的配置,就能完成定时器的任务。
package thread;import java.text.ParseException;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 ParseException {//创建定时器对象Timer timer = new Timer();//创建定时器对象并指定为守护线程// Timer timer = new Timer(true);// timer.schedule(定时任务,第一次执行时间,间隔多久执行一次)//注意:这里的第一次执行时间是到点才执行,不可以是过去的时间,否则从现在的时间算起SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSS");Date firstTime = sdf.parse("2021/10/13 11:35:32 893");//第一个参数其实可以使用匿名内部类timer.schedule(new LogTimerTask(),firstTime,1000*3);}}//单独编写一个定时器任务类//假设这是一个记录日志的定时任务class LogTimerTask extends TimerTask{@Overridepublic void run() {//编写你需要执行的任务就行了SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSS");String strTime = sdf.format(new Date());System.out.println(strTime + ":成功完成了一次数据备份!");}}//输出内容如下://2021/10/13 11:35:38 007:成功完成了一次数据备份!//2021/10/13 11:35:41 006:成功完成了一次数据备份!//2021/10/13 11:35:44 006:成功完成了一次数据备份!//2021/10/13 11:35:47 007:成功完成了一次数据备份!
关于Object类的wait和notify方法
几点说明
第一,wait和notify方法不是线程对象的方法,是Java中任何一个对象都有的方法,因为这2个方法是Object类中 自带的
第二、wait方法的作用:
Object o = new Object();
o.wait();
表示让o对象上活动的线程进入等待状态,无限期等待,直到被唤醒。
第三、notify方法的作用:
Object o = new Object();
o.notify();
表示唤醒正在o对象上等待的线程。
还有一个notifyAll()方法,这个方法是唤醒o对象上处于等待的所有线程
wait方法和notify方法是建立在synchronized线程同步的基础之上的
第四、wait方法和notify方法关于是否释放锁的区别?
o.wait()会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁;
o.notify()只会通知而不会释放之前占有的o对象的锁
生产者消费者案例★★
package thread;import java.util.ArrayList;import java.util.List;//模拟这样一个需求://仓库我们采用list集合,且这个集合有1个元素就表示满了,0个表示空了//必须做到这样的效果:生产一个,消费一个public class ThreadTest09 {public static void main(String[] args) {//创建1个仓库对象,共享的ArrayList list = new ArrayList();//创建2个线程对象//生产者线程Thread t1 = new Thread(new Producer(list));//消费者线程Thread t2 = new Thread(new Consumer(list));t1.setName("t1");t2.setName("t2");t1.start();t2.start();}}class Producer implements Runnable{private List list;public Producer(List list) {this.list = list;}@Overridepublic void run() {//一直生产(用死循环来模拟一直生产)while (true){synchronized (list){if (list.size()>0){//当前线程进入等待状态,且释放Producer之前占有的list集合的锁try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//程序能执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "--->" + obj);//唤醒消费者去消费// list.notify();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.notify();list.notifyAll();}}}}//内容输出:生产一个,消费一个//t1--->java.lang.Object@4b527522//t2-->java.lang.Object@4b527522//t1--->java.lang.Object@4a92f21c//t2-->java.lang.Object@4a92f21c//t1--->java.lang.Object@76238b4f//t2-->java.lang.Object@76238b4f....................
小练习:使用生产者和消费者模式实现交替输出
假设只有2个线程,输出以下结果:
t1—>1
t2—>2
t1—>3
t2—>4
t1—>5
t2—>6
要求:必须交替,并且t1线程输出奇数,t2线程输出偶数。
(提示:2个线程共享一个数字,每个线程执行时都要对这个数字进行加一操作)

