多线程(续集)
线程同步机制(synchronized)
//线程同步机制代码格式
synchronized(排队线程共享的对象){ 线程同步代码块 }
/*
():括号中填的是,排队线程共享的对象,比如有t1,t2,t3,t4,t5线程,只要线程t1,t2,t3排队执行,那么要在括号内写线程t1,t2,t3共享的对象,这个对象对于线程t4,t5是不共享的
*/
//取款的方法(写在账户类中)
public void widthBalance(double money){
//共享对象是账户,this代表当前账户
synchronized(this){
//取款前的账户余额
double before = this.getBalance();
//取款后的账户余额
double after = before - money;
try { //哪个线程先进来,哪个线程先睡1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
this.setBalance(after);
}
}
线程执行过程:
当线程 t1 执行到synchronized(this)
会自动找“共享对象”的对象锁并占用 this(即线程共享的Account对象) 的对象锁,每一个对象都有一个特定的对象锁(锁就是标记),此时线程 t2 也执行到synchronized(this)
获取对象锁,因为线程 t1 还在占用该对象锁,所以线程 t2 会等待,直到线程 t1 执行完同步代码块中的代码释放对象锁线程 t2 才继续往下执行。
线程执行到synchronized
代码处会在锁池中找共享对象的对象锁,线程进入锁池找共享对象的对象锁时,会释放之前占有的CPU时间片,如果没找到对象锁则在锁池中等待,如果找到了会进入就绪状态抢夺CPU时间片。(进入锁池可以理解为一种阻塞状态)
共享对象的深层理解:
public class Account {
private String actno;
private double balance;
public Account(){
}
public Account(String actno,double balance){
this.actno = actno;
this.balance = balance;
}
public void setActno(String actno){
this.actno = actno;
}
public String getActno(){
return actno;
}
public void setBalance(double balance){
this.balance = balance;
}
public double getBalance(){
return balance;
}
Object obj1 = new Object();
//取款的方法
public void widthBalance(double money){
Object obj2 = new Object();
//synchronized(this) {
//synchronized(obj1){
synchronized(obj2){
//取款前的账户余额
double before = this.getBalance();
//取款后的账户余额
double after = before - money;
//模拟网络延迟
try { //哪个线程先进来,哪个线程先睡1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
this.setBalance(after);
}
}
}
问题:
1、为什么使用synchronized(obj1)
能正常执行,而使用synchronized(obj2)
就不能正常执行呢?
答:obj1
是全局实例对象(Account 对象是多线程共享的,Account 对象中的实例变量 obj1 也是共享的),创建Account
实例对象时会同创建obj1
这个实例对象,此时这两个对象都只有一个,使用synchronized(obj1)
时线程会占用obj1
对象的锁,在同步代码块没有执行完时其他线程只能等待,所以obj1
也可以看成是共享对象。然而,obj2
是局部对象,每个线程执行widthBalance()
方法时都会创建一个obj2
对象,当执行synchronized(obj2)
时每个线程都可以找到相对应的对象锁,此时obj2
不是共享对象。
2、为什么synchronized("ac")
也能正常执行?
答:字符串常量池中ac
是唯一的,只有一个,但是此时字符串ac
是所有线程的共享对象,所有线程都会同步。
synchronized("ac")
和synchronized(this)
的区别:
//创建一个账户对象
Account act1 = new Account();
//创建线程
Thread t1 = new AccountThread(act1);
Thread t2 = new AccountThread(act1);
//创建另一个账户对象
Account act2 = new Account();
//创建线程
Thread t3 = new AccountThread(act2);
Thread t4 = new AccountThread(act2);
当synchronized("ac")
时,字符串ac
是 t1、t2、t3、t4 四个线程的共享对象。
当synchronized(this)
时,act1 是线程 t1、t2 的共享对象,act2 是线程 t3、t4 的共享对象。
synchronized(){}
中的同步代码块代码越少效率就越高。
同步代码的另一种写法:将widthBalance()
的整个方法作为同步代码块,这种方式增加了同步代码块的代码,效率更低 。
public class AccountThread extends Thread {
//线程共享的账户
private Account act;
//通过构造方法把账户对象传递过来
public AccountThread(Account act){
this.act = act;
}
/**
* 取款时调用的方法
*/
public void run(){
//取款的金额
double money = 5000;
synchronized(act){ //不能写this,this代表当前线程,不是共享对象
act.widthBalance(money);
}
System.out.println(Thread.currentThread().getName()+"对账户"
+act.getActno()+"取款"+money+",账户余额:"+act.getBalance());
}
}
synchronized
出现在实例方法上:(不常用)
public synchronized void widthBalance(double money){
Object obj2 = new Object();
//取款前的账户余额
double before = this.getBalance();
//取款后的账户余额
double after = before - money;
//模拟网络延迟
try { //哪个线程先进来,哪个线程先睡1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
this.setBalance(after);
}
synchronized
出现在实例方法上时,共享对象只能是this
,这种方式不灵活;这种方式表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低,所以这种方式不常用。
如果共享对象就是this
并且需要同步的代码块是整个方法体,则建议synchronized
使用在实例方法上,这样代码少,更加简洁。
例如:StringBuffer
的源代码中很多都是synchronized
出现在实例方法上,是线程安全的,而StringBuilder
是非线程安全的。
问题:使用局部变量(没有线程安全问题)时是用StringBuffer
还是StringBuilder
?
答:使用StringBuilder
。如果使用StringBuffer
每次都会进入锁池放弃CPU时间片或等待,这样执行效率大大降低,所以局部变量中尽量使用StringBuilder
。
补充: Vector、Hashtable
是线程安全的, ArrayList、HashMap、HashSet
是非线程安全的。
总结:synchronized
的三种写法:
第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
,表示共享对象一定是 this ,并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
,表示找类锁。(类锁只有一把)
面试题:doOther方法执行的时候需要等待doSome方法结束吗?
public class Test{
public static void main(String[] args){
//创建共享对象
TestClass tc = new TestClass();
//创建线程
Thread t1 = new TestThread(tc);
Thread t2 = new TestThread(tc);
t1.start();
try{
Thread.sleep(1000);//让主线程睡1秒,保证线程t1先执行
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
//测试类
class TestClass{
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");
}
}
//线程类
class TestThread extends Thread{
private TestClass tc;
public TestThread(TestClass tc){
this.tc = tc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
tc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
tc.doOther();
}
}
}
- doOther 方法没有
synchronized
,此时doOther 方法执行的时候不需要等待 doSome方法结束,线程 t1 调用 doSome 方法时占用 TestClass 的对象锁,当线程 t1 占用对象锁时线程 t2 调用 TestClass 对象的 doOther 方法,因为doOther 方法没有synchronized
调用时不需要等待 doSome 方法释放对象锁可以直接执行。
2)doOther 方法有synchronized
,此时线程 t2 调用 doOther 方法需要获取对象锁,所以必须等待 doSome 方法结束。
3)当synchronized
出现在静态方法上(找的是类锁),这时需要等,因为静态方法找类锁,虽然两个线程执行的是两个对象,但这两个对象同属于一个 TestClass 类,所以线程 t1 占用类锁时,线程 t2 必须等待。
public class Test{
public static void main(String[] args){
//创建共享对象
TestClass tc1 = new TestClass();
TestClass tc2 = new TestClass();
//创建线程
Thread t1 = new TestThread(tc1);
Thread t2 = new TestThread(tc2);
t1.start();
try{
Thread.sleep(1000);//让主线程睡1秒,保证线程t1先执行
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
//测试类
class TestClass{
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");
}
}
//线程类
class TestThread extends Thread{
private TestClass tc;
public TestThread(TestClass tc){
this.tc = tc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
tc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
tc.doOther();
}
}
}
死锁(重点)
线程1,2都需要同时锁住对象1,2才能顺利执行下去,且线程1是先锁住对象1再锁对象2,线程2是先锁对象2再锁对象1,但线程1,2同时执行时就无法同时将对象1,2同时锁住,此时程序不出现异常,也不出现错误,程序一直僵持很难调试出错误。
死锁代码:(必须会手写)
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 MyThread2o1,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 MyThread1(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){
}
}
}
}
怎样合理的解决线程安全问题?
使用线程同步机制(synchronized
)会降低程序执行效率,系统的用户吞吐量(并发量)降低,用户体验差,在不得已的情况下才选择线程同步机制。
方案一:尽量使用局部变量代替“实例变量和静态变量”。(局部变量不共享)
方案二:当必须使用实例变量时,可以考虑创建多个对象,一个线程对应一个对象,这样实例变量的内存就不共享了,就没有数据安全问题了。
方案三:如果不能使用局部变量,对象也不能创建多个,此时只能使用synchronized
线程同步机制。
守护线程
Java 中的线程分为两大类:用户线程(如主线程main)、守护线程(后台线程,如垃圾回收线程)
守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
守护线程用在哪?怎么用?
答:例如每天零点时系统数据自动备份。我们可以将定时器设置为守护线程,每次一到零点的时候就备份一次,当所有的用户线程结束后,守护线程自动退出。
//守护线程测试
public class Test{
public static void main(String[] args){
Thread t = new DataThread();
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 DataThread 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();
}
}
}
}
定时器
定时器的作用:间隔特定的时间,执行特定的程序。
比如每周进行银行账户的总账操作;每天进行数据的备份工作。
实际开发中,每隔一定时间执行特定的程序在Java 中可以采用多种方式实现:
1、使用sleep
睡眠方法,这是最原始的定时器。
2、java.util.Timer
Java类库中已经写好的定时器,开发中很少用,因为很多高级框架都是支持定时任务的。
3、实际开发中使用较多的是 Spring 框架中提供的 SpringTask 框架,只要进行简单配置就可以完成定时的任务。
public class TimerTest{
public static void main(String[] args){
//创建定时器对象
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-07-30 10:00:00");
timer.schedule(new LogTimerTask(),firstTime,1000 * 10);
}
}
//编写一个定时任务类,假设这是个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run(){
//在此处编写需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String steTime = sdf.format(new Date());
System.out.println(strTime+":成功完成了一次数据备份!");
}
}
结果:从2020-07-30 10:00:00开始,每隔10秒完成一次数据备份。
实现线程的第三种方式
拿到线程的执行结果,可以通过实现callable
接口的方式(JDK8 的新特性)。
优点:可以获取线程的执行结果。
缺点:效率较低,在获取线程 t 执行结果时,当前线程受阻塞效率低。
call()
方法相当于run()
方法,但call()
方法有返回值。
public static void main(String[] args){
//创建一个“未来任务类”对象,参数是Callable接口实现类对象
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 end");
int a = 1;
int b = 4;
return (a+b);//结果是Integer类型(自动装箱)
}
});
//创建线程对象
Thread t = new Thread(task);
t.start();
//在主线程中获取线程t的返回结果
Object obj = task.get();//此处抛出一个异常
System.out.println("线程t执行结果:"+obj);
System.out.println("主线程");
}
问题:在主线程中获取线程t的返回结果的get()
方法会不会导致主线程阻塞?
答:会。主线程要继续执行下去必须等待get()
方法结束,返回另一个线程的执行结果需要一定的时间,所以会阻塞主线程。
2.8.10 关于 Object 类中的 wait 和 notify 方法
1、wait()
方法作用
Object obj = new Object();
obj.wait();
表示让正在 obj 对象上活动的线程进入等待状态,并且释放之前占有的 obj 对象的锁,无限期等待,直到被唤醒为止。
2、notify()
方法作用
Object obj = new Object();
obj.notify();
唤醒正在 obj 对象上等待的线程(如果有多个线程处于等待状态则会随机唤醒一个线程),不会释放之前占有的 obj 对象的锁。
还有一个notifyAll()
方法,唤醒 obj 对象上处于等待的所有线程。
生产者和消费者模式:
1、什么是生产者和消费者模式
生产线程负责生产,消费线程负责消费,生产线程和消费线程达到均衡。
2、使用wait()
和notify()
方法实现“生产者和消费者模式”
因为多线程要同时操作一个仓库(共享对象),有线程安全问题,所以wait()
和notify()
方法建立在线程同步(排队执行)的基础上。
练习:
public class WakeThread {
public static void main(String[] args) {
/**
* 创建集合
*/
List list = new ArrayList();
/**
* 创建线程
*/
Thread t1 = new Thread(new Producer(list));
Thread t2 = new Thread(new Comsumer(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){
try {
//集合中有元素生产者线程进入等待状态,并释放list对象锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当集合中没有元素时进行生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"生产了元素:"+obj);
//唤醒消费线程消费
list.notify();
}
}
}
}
问题:
1、为什么上述代码中唤醒线程可以用list.notifyAll();
代替list.notify();
?
答:因为生产线程和消费线程都进行了集合中元素的判断,并且都有wait()
方法,所以即使唤醒全部线程也不会造成线程并发。
2、消费者中唤醒生产者时,是否会再次立即抢到锁?
答:消费者线程可能会再次立即抢到锁。但此时集合中元素为0,消费者线程会进入等待状态并释放对象锁,生产线程进行生产。