第六章
- 多线程概述
- 实现线程有两种方式
- 线程对象的生命周期
- 线程的常用方法
- 线程的安全synchronized(重点)
- 死锁
- 线程这块的其他内容
多线程概述
- 什么是进程?什么是线程?、
进程是一个应用程序(1个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程
- 对于java程序来说,当在DOS命令窗口中输入:
java Helloworld 回车之后
会先启动JVM,而JVM就是一个进程。
JVM会再启动一个主线程,调用main方法。
同时会再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中有两个线程并发。
- 进程与进程和线程与线程是是什么关系?
进程A和进程B的内存独立不共享。(魔兽游戏与酷狗音乐)
线程A和线程B内存,堆内存和方法区内存共享。栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,栈与栈之间互不干扰,各自执行各自的,这就是多线程并发。
-
思考一个问题:
使用了多线程机制之后,main方法结束,是不是有可能出现也不会结束。
main方法结束只是主线程结束了主栈空了,其他的栈(线程)可能还在压栈弹栈。
-
分析一个问题:对于单核的cpu来说,真的可以做到真正的多线程并发吗?
什么是真正的多线程并发?
t1线程执行t1的
t2线程执行t2的
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的cpu表示只有一个大脑。
- java语言中,实现线程有两种方式,哪两种方式呢?
方式一:编写一个类,直接继承java.lang.Thread,重写run方法。
方式二:编写一个类实现java.lang.Runnable接口。(常用!!!)
实现线程有两种方式
/**
* 大家分析一下程序,有几个线程,除垃圾回收线程之外,有几个线程?
* 一个线程(因为程序只有一个栈)
*
* main begin
* m1 begin
* m2 begin
* m3 execute!
* m2 over
* m1 over
* main over
* 一个栈中自上而下的顺序依次逐行执行。
*/
public class ThreadTest01 {
public static void main(String[] args) {
System.out.println("main begin");
m1();
System.out.println("main over");
}
private static void m1() {
System.out.println("m1 begin");
m2();
System.out.println("m1 over");
}
private static void m2() {
System.out.println("m2 begin");
m3();
System.out.println("m2 over");
}
private static void m3() {
System.out.println("m3 execute!");
}
}
方式一:编写一个类,直接继承java.lang.Thread,重写run方法。
/**
* 实现线程的第一种方式:
* 编写一个类,直接继承java.lang.Thread,重写run方法。
*
* 怎么创建线程对象? new就行了。
* 怎么启动线程? 调用线程对象的start()方法。
*
* 注意:
* 亘古不变的道理:
* 方法体当中的代码永远都是自上而下的顺序。
*/
public class ThreadTest02 {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行。
//新建一个分支线程对象。
MyThread myThread = new MyThread();
//启动线程
//myThread.run();//直接调run不会启动线程,不会分配新的分支栈。(这种方式就是单线程的)
//start()方法的作用是:启动一个分支线程,在JVM中开辟一个栈空间,这段代码任务完成之后,瞬间就结束了。
//启动成功的线程会自动调用run方法,并且run方法在分支栈的底部(压栈)。run和main是平级的,实现了两个线程并发。
myThread.start();
//这里的代码还是运行在主线程中。
for (int i=0;i<1000;i++){
System.out.println("主线程->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)。
for(int i=0;i<1000;i++){
System.out.println("线程分支->"+i);
}
}
}
/*
运行结果:通过调用start()方法,明显看得出main线程和分支线程是并发执行的。
...
主线程->7
主线程->8
线程分支->0
线程分支->1
...
*/
直接调run的内存图:
调用start的内存图:
方式二:编写一个类实现java.lang.Runnable接口。(常用!!!)
/**
* 实现线程的第二种方式,编写一个类实现java.lang.Runnable接口。
*
*/
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{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("分支线程-->"+i);
}
}
}
/*
运行结果:
...
主线程-->7
主线程-->8
分支线程-->0
分支线程-->1
主线程-->9
...
*/
/**
* 采用匿名内部类可以吗? 可以!
*
*/
public class ThreadTest04 {
public static void main(String[] args) {
Thread t =new Thread(new Runnable() {
@Override
public void run() {
for (int i =0;i<100;i++){
System.out.println("分支线程-->"+i);
}
}
});
//启动分支线程
t.start();
for (int i=0;i<100;i++){
System.out.println("主线程-->"+i);
}
}
}
/*
运行结果:
...
主线程-->4
主线程-->5
分支线程-->0
分支线程-->1
...
*/
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他类,更灵活。
关于线程对象的生命周期?(面试会问!!)
关于线程对象的生命周期?
新建状态
就绪状态
运行状态
阻塞状态
死亡状态
线程的常用方法
/**
* 1、怎样获取当前线程对象?
* Thread t = Thread.currentThread();
* 返回值t就是当前线程。
* 2、获取线程对象的名字
* String name = 线程对象.getName();
* 3、修改线程对象的名字
* 线程对象.setName("线程名字");
*
* 4、当线程没有设置名字,默认的有什么规律?
* Thread-0
* Thread-1
* Thread-2
* Thread-3
* ......
*/
public class ThreadTest05 {
public static void main(String[] args) {
//获取当前线程对象
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName());//main
//创建线程对象
MyThread2 t = new MyThread2();
//设置线程的名字
//t.setName("tttt");
//获取线程的名字
System.out.println(t.getName());//Thread-0
//启动线程
t.start();
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
Thread currentThread = Thread.currentThread();
//哪个线程启动它,哪个线程就是当前线程。t.start(); t就是当前对象
System.out.println(currentThread.getName()+"--->"+i);
}
}
}
线程的sleep方法
/**
* 关于线程的sleep方法:
* static void sleep(long millis);
* 1、静态方法
* 2、参数是毫秒
* 3、作用:让当前线程进入休眠,进入“阻塞”状态,放弃占用cpu片段,让给其他线程使用。
*/
public class ThreadTest06 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("分支线程");//5秒之后执行
}
});
//启动线程
t.start();
System.out.println("hello world");
System.out.println("============================");
for (int i=0;i<10;i++){
//睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
/*
运行结果:
hello world
============================
main-->0
main-->1
main-->2
main-->3
分支线程
main-->4
main-->5
main-->6
main-->7
main-->8
main-->9
*/
sleep相关面试题
/**
* Thread.sleep()方法的面试题
*/
public class ThreadTest07 {
public static void main(String[] args) {
//创建线程对象
MyThread3 t = new MyThread3();
t.setName("t");
//启动
t.start();
//调用sleep方法
try {
//问题:这行代码会让线程t进入休眠状态码? 不会,因为sleep是静态方法
t.sleep(1000*5);//在执行的时候还是会转换成:Thread.sleep(1000*5);
//这行代码作用是让当前线程进入休眠,也就是main线程。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");//5秒之后输出
}
}
class MyThread3 extends Thread{
@Override
public void run() {
for (int i=0;i<10000;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
终止sleep方法
/**
* sleep睡太久了,如果希望半道醒来,你应该怎么办?也就是怎么叫醒正在睡眠的线程。
* 注意:这个不是终止线程的执行,是终止线程的睡眠。
*/
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();//启动
//希望5秒之后,t线程醒来。
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终断t线程的睡眠(这种终断的方式依靠了java的异常处理机制,使其直接进入catch语句)
t.interrupt();//
System.out.println("main over");
}
}
class MyRunnable2 implements Runnable{
//重点:为什么这里不能throws异常,只能try..catch?因为子类重写父类方法,子类不能比父类抛出更多的异常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->begin");
try {
Thread.sleep(1000*365*24*60*60);//睡眠1年
} catch (InterruptedException e) {
//打印异常信息
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->end");
}
}
强行终止线程
/**
* 怎样合理的终止一个线程的执行。这种方式是很常用的。
*/
public class ThreadTest10 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
//模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
r.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);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//return就结束了
//这里可以保存 save...
//终止当前线程
return;
}
}
}
}
线程调度的方法
java中提供哪些方法是和线程调度有关系的呢?
实例方法:
实例方法:
void setPriority(); //设置线程的优先级
int getPriority(int newPriority); //获取线程优先级
/*
最低优先级1
默认优先级5
最高优先级10
优先级比较高的获取CPU时间片可能会多一些。
*/
静态方法:
static void yield(); //让位方法
/*
暂停当前正在执行的线程对象,并执行其他线程。
yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪状态之后,有可能还会再次抢到。
实例方法:
void join()
合并线程
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t = new MyThread2();
MyThread2 t2 = new MyThread2();
t.join();//当前线程进入阻塞,t线程执行,直到t线程结束,当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}
*/
线程的优先级:
/**
* 了解:关于线程的优先级
*/
public class ThreadTest11 {
public static void main(String[] args) {
//设置主线程优先级为1
Thread.currentThread().setPriority(1);
/*System.out.println("最高优先级:"+Thread.MAX_PRIORITY);//10
System.out.println("最低优先级:"+Thread.MIN_PRIORITY);//1
System.out.println("默认优先级:"+Thread.NORM_PRIORITY);//5*/
//获取当前线程对象的优先级
//System.out.println(Thread.currentThread().getPriority());//5
Thread t = new Thread(new MyRunnable5());
t.setPriority(10);
t.setName("t");
t.start();
//优先级高的,只是抢到的CPU的时间片相对多一些。
//大概率是偏向优先级高的。
for (int i=0;i<1000;i++){
System.out.println("主线程--->"+i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
//获取线程优先级
System.out.println(Thread.currentThread().getName()+"线程的默认优先级:"+Thread.currentThread().getPriority());//5
for (int i=0;i<1000;i++){
System.out.println("分支线程--->"+i);
}
}
}
线程让步(yield)
/**
* 让位,当线程暂停,回到就绪状态,让给其他线程。
* 静态方法:Thread.yield();
*
*/
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
//main线程让步
Thread.yield();
for (int i=0;i<10;i++){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"--->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
for (int i =0;i<10;i++){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"-->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程的合并/阻塞方法(join)
/**
* join();
* 合并/阻塞 线程的方法
*/
public class ThreadTest13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyRunnable7());
t.setName("t");
t.start();
//线程合并/阻塞 当前线程
t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行直到结束,
for (int i=0;i<10;i++){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"-->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable7 implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"-->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程的安全(重点)
什么时候数据在多线程并发的情况会出现异常?
三个条件:
条件1:多线程并发。
条件2:有共享的数据。
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
怎么解决线程安全问题?
线程排队执行(不能并发)。
这种机制被称为:线程同步机制。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率。(先考虑安全,在考虑效率!!)
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1。
谁也不需要等谁,其实就是多线程并发(效率较高)。
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程结束,
或者说在t2线程执行的时候,必须等待t1线程结束。两个线程之间发生了等待关系,
这就是同步编程模型,效率较低,线程排队执行。
记住:异步就是并发,同步就是排队!
模拟银行取款(编写两个线程)重要!!!
多线程不安全的情况:
账户类:
/*
银行账户
不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
*/
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;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public void withdraw(double money){
//取款之前
double before=this.getBalance();
//取款之后
double after=before-money;
//在这里模拟一下网络延迟,100%会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这了,但是还没来得及执行改行修改代码,t2线程进来了该方法
//此时一定出问题
this.setBalance(after);
}
}
线程类:
public class AccountThread extends Thread{
//两个线程必须共享同一个账户对象
private Account account;
//通过构造方法传过来
public AccountThread(Account account) {
this.account = account;
}
@Override
public void run() {
//t1和t2并发这个方法。。。(t1和t2是两个栈,两个栈操作堆中同一个对象。)
//run方法执行表示取款操作。
//假设取款5000
double money = 5000;
//取款
//多线程并发执行这个方法
account.withdraw(money);
System.out.println("线程"+Thread.currentThread().getName()+"取款成功,剩余"+account.getBalance()+"元");
}
}
测试类:
/**
* 模拟银行取款(编写两个线程)
*/
public class Test {
public static void main(String[] args) {
//创建账户
Account account = new Account("wl",10000);
//创建两个线程对象
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
System.out.println(account.getBalance());//这里输出是10000,因为start执行是一瞬间。
}
}
/*
运行结果:
10000.0
线程t1取款成功,剩余5000.0元
线程t2取款成功,剩余5000.0元
*/
改进后的线程安全的情况:
账户类:
/**
* 账户类
*/
public class Account {
//账户
private String actno;
//余额
private double balance;
private Object obj = new Object();
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;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public void withdraw(double money){//取款
//以下这几行代码必须是线程排队,不能并发。
//一个线程把这里的代码全部指向结束之后,另一个线程才能进来。
/*
线程同步/排队机制的语法是:
synchronized (){
//线程同步代码块。
}
synchronized后面小括号传的这个"数据"是相当关键的。
这个数据必须是多线程共享的数据。才能达到多线程排队。
()中写什么?
那要看你想让那些线程同步。
假设t1、t2、t3、t4、t5有5个线程
你只希望t1、t2、t3排队,t4、t5不要排队,怎么办?
你一定要在()中写一个t1、t2、 t3共享的对象。而这个对象
对于t4、t5来说不是共享的。
这里共享的是:账户对象
账户对象是共享的,namethis就是账户对象!!!
不一定是this,只要是共享的那个对象就行了。
在java语言中,任何一个对象都有“一把锁”,其实锁就是一个标记。
100个对象100把锁。1个对象1把锁。
1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
2、假设t1先执行了,遇到synchronized,这个时候自动找共享对象的对象锁,
找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中
一直都占有这把锁,直到同步代码块执行结束后,这把锁才会释放。
3、假设t1已经占有了这把锁,此时t2也遇到synchronized关键字,也会去
占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面
等待t1的结束,直到t1把同步代码块结束了。此时t2终于等到这把锁,然后t2
占有这把锁之后,进行同步代码块的执行。这样就实现了同步排队执行。
*/
Object obj2 = new Object();
//synchronized (obj) 可以
//synchronized (obj2) 不可以,因为每个线程的obj2都不是一个共享数据对象!!
//synchronized ("abc") 可以 因为在字符串常量池,只有一个。 但是所有线程都会同步。
//synchronized (null) 不可以 会报错 空指针异常。
synchronized (this){
//取款前
double before=this.getBalance();
//取款后
double after=before-money;
//这里存在线程安全隐患问题!!!
//取款
this.setBalance(after);
}
注意:这里的锁池不是一种状态,可以理解为一种堵塞之一。
线程类:
public class AccountThread extends Thread{
private Account account;
public AccountThread(Account account) {
this.account = account;
}
@Override
public void run() {//线程调用account对象的取款方法
double money = 5000;
account.withdraw(money);//取款5000
System.out.println("线程"+Thread.currentThread().getName()+"取款成功,剩余"+account.getBalance()+"元");
}
}
测试类:
public class Test {
public static void main(String[] args) {
Account account = new Account("王磊",10000);
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
System.out.println(" "+account.getBalance());
}
}
/*
运行结果:
线程t2取款成功,剩余0.0元
线程t1取款成功,剩余0.0元
10000.0
或
线程t2取款成功,剩余0.0元
10000.0
线程t1取款成功,剩余5000.0元
...(线程安全的)
*/
synchronized(){}中()传this和"abc"的区别:
传"abc"的话,所有的线程都要排队。
传this的话,共享同一个this当前对象的线程需要排队,不共享的不用排队。如以下代码,t1和t2共享account
需要排队,而t3和t1、t2不共享同一个对象,因此t3不需要排队。
public class Test {
public static void main(String[] args) {
Account account = new Account("王磊",10000);
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
Account account2 = new Account("李四",10000);
AccountThread t3 = new AccountThread(account2);
t1.start();
t2.start();
System.out.println(" "+account.getBalance());
}
}
哪些变量存在线程安全问题?
1、局部变量永远都不会存在线程安全问题,因为局部变量不共享。(一个线程一个栈。),栈中的元素不共享。
2、实例变量在堆中,堆只有1个。
3、静态变量在方法区中,方法区只有1个
4、常量没有线程安全问题,因为常量不可修改。
堆和方法区都是多线程共享的,所有可能存在多线程安全问题。
synchronized(account){
account.withdraw();
}
以上写法扩大synchronized同步代码块的范围是可以的,但是效率更低了。
把account换成this可以吗? 不可以
因为这里的this是AccountThread对象,这个对象不共享!该线程对象new了两次。
在实例方法上出现synchronized关键字
账户类:
public synchronized void withdraw(double money){
//取款之前
double before=this.getBalance();
//取款之后
double after=before-money;
//在这里模拟一下网络延迟,100%会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这了,但是还没来得及执行改行修改代码,t2线程进来了该方法
//此时一定出问题
this.setBalance(after);
}
分析这种方式:
synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他对象了。
另外,还有一个缺点:synchronize出现在实例方法上,表示整个方法体都需要同步,可能会
无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
优点:代码写得少了。
如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
如果要使用局部变量,是选择StingBuffer还是StringBuilder?
StringBuilder,因为局部变量没有线程安全问题,同时StringBuilder的方法没有synchronized修饰,
执行效率更高。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap、HashSet是非线程安全的。
Hashtable是线程安全的。
总结synchronized用法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this。
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,类锁也只有1把。
注意:
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把锁
对象锁用来保护实例变量的安全。
类锁用来保护静态变量的安全。
synchronized面试题
练习1:以下代码中,线程t2会不会等待t1执行完再执行?
/**
* 面试题1
*/
public class exam01 {
public static void main(String[] args) {
MyClass c = new MyClass();
MyThread t1 = new MyThread(c);
MyThread t2 = new MyThread(c);
t1.setName("t1");
t2.setName("t2");
//启动
t1.start();
try {
Thread.sleep(1000);//保证t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private MyClass c;
public MyThread(MyClass c) {
this.c = c;
}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
c.doSome();
}else {
c.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!");
}
}
/*
运行结果:
doSome begin!
doOther begin!
doOther over!
doSome over!
*/
答:不会,因为doOther方法没有被synchronized修饰。
练习2:
将练习1的doOther方法用synchronized关键字修饰,线程t2会不会等待t1执行完再执行。 会。
练习3:
将练习2的Test方法中,new 两个MyClass对象,分别传给两个线程,线程t2会不会等待t1执行完再执行。 不会。
因为两个对象两把锁。
练习4:将练习3的doSome方法改成静态static方法,类锁。线程t2会不会等待t1执行完再执行? 会。
因为静态方法是类锁,不管创建几个对象,类锁只有1把。
排他锁 synchronized
互斥锁
死锁(重点)
/**
*死锁代码要会写(面试要写)。
* 因为死锁很难调试。
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
MyThread1 t1 = new MyThread1(o1, o2);
MyThread1 t2 = new MyThread1(o1, o2);
t1.setName("t1");
t2.setName("t2");
//启动
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
private Object o1;
private 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{
private Object o1;
private 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会让程序的执行效率降低,用户体验不好,系统的用户吞吐量(并发量)降低,
是在不得已的情况才选择的。
秒杀功能 12306 (排队就用户体验不好了),所有还是选择异步(并发)方式,但是不安全!
第一种解决方案:尽量使用局部变量代替“实例变量”和“静态变量”。
第二种方案:如果必须是实例变量,那么就考虑创建多个对象,这样实例变量的内存就不共享了
(一个线程对应一个对象,100个线程对应100个对象,对象不共享就没有数据安全问题了。)
第三章方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。
线程这块还有哪些其他内容?
- 守护线程
- 定时器
- 实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)
- 关于Object类中的wait和notify方法。
守护线程
java语言中的守护线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
守护线程的特点:
一般守护线程是个死循环,所有的用户线程只要结束,
守护线程自动结束。
注意:主程序main方法是一个用户线程。
守护线程用在什么地方呢?
守护线程的使用:
t.setDaemon(true);
/*
设置守护线程
*/
public class ThreadTest14 {
public static void main(String[] args) {
BakDataThread 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{
@Override
public void run() {
int i=0;
//即使是死循环,但由于该线程是守护线程,当用户线程结束,守护线程自动终止。
while (true){
System.out.println("计时器--->"+(i++));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定时器
定时器的作用:间隔特定的时间去执行特定的程序。
可以使用sleep方法,睡眠,设置睡眠时间。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用,这种方式也很少用
在实际的开发中,目前使用较多的是Spirng框架中提供的SpringTask框架,
在这个框架中只要进行简单的配置,就可以完成定时器的任务。
实现线程的第三种方式
实现Callable接口:
这种方式实现的线程可以获取线程的返回值,之前的那两种方式是无法获取返回值的,因为run方法返回的是void。
思考:系统委派一个线程去执行一个任务,这个线程执行完之后,可能会有体格执行结果,怎么获取这个执行结果呢?使用线程的第三种方式Callable接口。
优点:可以获取线程执行的返回值。
缺点:效率低,因为调用get()方法会导致当前线程阻塞。
/**
* 实现线程的第三种方式:
* 实现Callable接口
*/
public class ThreadTest15 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个"未来任务类"对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call方法就相当于run方法,只不过这个有返回值。
//线程执行一个任务,执行结束后可能有一个执行结果。
//模拟执行
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method over");
int a = 100;
int b = 200;
return a+b;//自动装箱(300结果编程Integer)
}
});
//创建线程对象
Thread t = new Thread(task);
t.setName("t");
t.start();
//这里是main方法,这是在主线程中。
//在主线程中,怎么获取t线程的返回结果呢?
//get()方法的执行会导致当前线程的阻塞。
Object obj = task.get();
System.out.println("线程执行结果"+obj);
//main方法这里的程序想要执行必须等待get()方法的结束,
//而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果。
//另一个线程的执行是需要时间的。
System.out.println("main over");
}
}
Object类中的wait和notify方法
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法。,因为这两个方法
是Object自带的。
Object o = new Object();
o.wait();
o.notify();
表示:
让正在o对象上活动的线程进入等待状态,无期限等待。
直到调用notify()方法唤醒线程对象。notifyAll()方法用来唤醒o对象上处于等待的所有
线程。
生产者与消费者模式均衡(难点)
/**
* 1、使用wait方法和notify方法实现“生产者和消费者模式”
* 2、什么是生产者和消费者模式?
* 生产线程负责生产,消费线程负责消费。
* 生产线程和消费线程要达到均衡,在这种特殊的情况下需要
* 使用wait()和notify()方法,
* 3、这两个该方法不是线程对象的方法,是普通java对象都有的方法。
* 4、wait()方法和notify()方法建立在线程同步的基础之上。因为
* 多线程要同时操作一个仓库,有线程安全问题。
* 5、wait()方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并释放t线程之前所占有的o对象的锁。
* 6、notify()方法的作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前所占有的锁。
* 7、模拟这样一个需求:
* 仓库我们采用List集合。
* List集合中假设只能存1个元素。
* 必须达到:生产1个消费1个。
*
*/
public class ThreadTest16 {
public static void main(String[] args) throws InterruptedException {
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;
}
@Override
public void run() {
while (true){
synchronized (list){
if (list.size()>0){//大于0,说明仓库中已经有了一个元素。
try {
list.wait();//当前线程进入等待状态,并且释放list集合的锁。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这里说明仓库是空的,可以生产。
Object o = new Object();
list.add(o);
System.out.println(Thread.currentThread().getName()+"--->"+o);
//唤醒消费者
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.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这里,代表仓库已经满了,可以消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
//唤醒生产者
list.notifyAll();
}
}
}
}
练习:使用生产者和消费者模式,交替输出。