多线程
概述
进程是程序执行的最小单位,线程是进程的一部分,即一个进程对应多个线程,一个进程也可以启动多个线程。对于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{
@Override
public 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{
@Override
public 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不再是接口,而是一个匿名内部类,所以可以new
Thread t = new Thread(new Runnable() {
@Override
public 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() {
@Override
public 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()); //main
MyThread2 mt2 = new MyThread2();
//若没有设置线程名字,则默认为:Thread-0
mt2.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{
@Override
public 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{
@Override
public 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 some
Test test = new Test();
test.doSome(); //do some
test = 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{
@Override
public 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{
@Override
public 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;
@Override
public 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{
@Override
public 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{
@Override
public 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;
}
@Override
public void run() {
//run方法的执行表示取款操作,这里假设取款5000
double 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;
}
@Override
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*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;
}
@Override
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*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;
}
@Override
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*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;
}
@Override
public 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,o2
Thread 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;
}
@Override
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;
}
@Override
public 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{
@Override
public 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{
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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个线程共享一个数字,每个线程执行时都要对这个数字进行加一操作)