//线程同步机制代码格式
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();
}
}
}
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.TimerJava类库中已经写好的定时器,开发中很少用,因为很多高级框架都是支持定时任务的。
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()方法结束,返回另一个线程的执行结果需要一定的时间,所以会阻塞主线程。
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,消费者线程会进入等待状态并释放对象锁,生产线程进行生产。
原文:https://www.cnblogs.com/gyunf/p/13423613.html