当前位置: 首页 > news >正文

JavaSE-11笔记【多线程2(+2024新)】

文章目录

  • 6.线程安全
    • 6.1 线程安全问题
    • 6.2 线程同步机制
    • 6.3 关于线程同步的面试题
      • 6.3.1 版本1
      • 6.3.2 版本2
      • 6.3.3 版本3
      • 6.3.4 版本4
  • 7.死锁
    • 7.1 多线程卖票问题
  • 8.线程通信
    • 8.1 wait()和sleep的区别?
    • 8.2 两个线程交替输出
    • 8.3 三个线程交替输出
    • 8.4 线程通信-生产者和消费者模式
  • 9.线程生命周期的回顾与补充
  • 10.单例模式
    • 10.1 饿汉式单例模式
    • 10.2 懒汉式单例模式
    • 10.3 懒汉式单例模式可能会造成线程安全问题
  • 11.可重入锁ReentrantLock
  • 12.实现线程的第三种方式:实现Callable接口
  • 13.实现线程的第四种方式:使用线程池

接上一篇 JavaSE-10笔记【多线程1】

6.线程安全

6.1 线程安全问题

  1. 什么情况下需要考虑线程安全问题?
    多线程并发的环境下,有共享的数据,且涉及到共享数据的修改操作。
    一般情况下:
    ①局部变量若为基本数据类型,则不存在线程安全问题【在栈中,栈不是共享的】;而若为引用数据类型则另说了。
    ②实例变量可能存在线程安全问题,实例变量在堆中,堆是多线程共享的。
    ③静态变量也可能存在安全问题,静态变量在堆中,堆是多线程共享的。

多个线程并发对同一个银行账户进行取款操作时,会有安全问题:t1线程在取了一笔钱后,由于网络卡顿,没有及时更新余额,此时t2线程又将未更新的数据读取到,取了一笔钱,导致两个线程都取出了一笔钱,而余额只少了一笔。
如何解决:
将t1线程和t2线程排队执行,不要并发,要排队。这种排队机制被称作“线程同步机制”。【对于t1线程和t2线程,t2线程在执行的时候必须等待t1线程执行到某个位置之后,t2线程才能执行。t1和t2之间发生了等待,则认为是同步。
如果不排队,则被称为“线程异步机制”,t1和t2线程各自执行各自的,并发执行,互相不需要等待。
异步:效率高,但不安全。
同步:安全,但效率低。

示例代码:

package threadtest.thread14;public class ThreadTest14 {public static void main(String[] args) {//创建账户对象Account account = new Account(54234657, 10000);//t1线程和t2线程共享一个账户Thread t1 = new Thread(new MyRunnable(account));t1.setName("t1");Thread t2 = new Thread(new MyRunnable(account));t2.setName("t2");//启动线程t1.start();t2.start();}
}class MyRunnable implements Runnable{private Account account;public MyRunnable(Account account) {this.account = account;}@Overridepublic void run() {account.withDraw(1000);}
}
class Account{private int actNo; //账户编号private double balance; //账户余额public Account(int actNo, double balance) {this.actNo = actNo;this.balance = balance;}public int getActNo() {return actNo;}public void setActNo(int actNo) {this.actNo = actNo;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/*** 取款* @param money 取款额度*/public void withDraw(double money){//为了演示出多线程并发带来的安全问题,这里将取款分为两步//1. 获取余额double before  = this.getBalance();System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前账户" + this.getActNo() + "的账户余额为" + before);try {Thread.sleep(1000); //为了演示线程安全问题,这里让当前线程睡眠1秒} catch (InterruptedException e) {throw new RuntimeException(e);}//2. 取款后修改余额this.setBalance(before - money);System.out.println(Thread.currentThread().getName() + "线程取款成功,当前账户" + this.getActNo() + "的账户余额为" + this.getBalance());}
}

运行结果(取款2次,余额有误):
在这里插入图片描述

6.2 线程同步机制

使用线程同步机制(本质是线程排队执行),来避免多线程并发的线程安全问题:

语法格式:

synchronized(必须是需要排队的这几个线程共享的对象){//需要同步的代码
}

必须是需要排队的这几个线程共享的对象必须选对了,如果选错了可能会无故增加同步线程的数量,导致效率降低。
原理:

synchronized(obj){//需要同步的代码
}

假设obj是t1和t2两个线程共享的,t1和t2执行这段代码的时候,一定有先后顺序的,一定是有一个先抢到了CPU时间片。

  • 假设t1先抢到了CPU时间片,t1线程找共享对象obj的对象锁,找到之后,占有这把锁,只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
  • 当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
  • 同样,t2线程抢到CPU时间片之后执行到这段代码也会去找对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待,直到t1归还锁才能执行同步代码块的代码。

注意:不要无故扩大同步代码块的范围,其范围越小,效率越高。

示例代码:

package threadtest.thread14;public class ThreadTest14 {public static void main(String[] args) {//创建账户对象Account account = new Account(54234657, 10000);//t1线程和t2线程共享一个账户Thread t1 = new Thread(new MyRunnable(account));t1.setName("t1");Thread t2 = new Thread(new MyRunnable(account));t2.setName("t2");//启动线程t1.start();t2.start();}
}class MyRunnable implements Runnable{private Account account;public MyRunnable(Account account) {this.account = account;}@Overridepublic void run() {account.withDraw(1000);}
}
class Account{private int actNo; //账户编号private double balance; //账户余额public Account(int actNo, double balance) {this.actNo = actNo;this.balance = balance;}public int getActNo() {return actNo;}public void setActNo(int actNo) {this.actNo = actNo;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/*** 取款* @param money 取款额度*/public void withDraw(double money) {//为了演示出多线程并发带来的安全问题,这里将取款分为两步synchronized (this) { //由于这里两个线程共享的是同一个account对象,所以这里可以直接填this,其他情况要再另外分析,并不一定都是填this!//1. 获取余额double before = this.getBalance();System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前账户" + this.getActNo() + "的账户余额为" + before);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//2. 取款后修改余额this.setBalance(before - money);System.out.println(Thread.currentThread().getName() + "线程取款成功,当前账户" + this.getActNo() + "的账户余额为" + this.getBalance());}}
}

也可以在实例方法上添加synchronized关键字:

  1. 在实例方法上添加了synchronized关键字之后,整个方法体就是一个同步代码块。
  2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是当前对象this的对象锁。

这种方式相对于上面的局部同步代码块的方式要差一些,利用局部同步代码块的优点:

  • 共享对象可以随便调整;
  • 同步代码块的范围可以随便调整。

示例代码:

package threadtest.thread14;public class ThreadTest14 {public static void main(String[] args) {//创建账户对象Account account = new Account(54234657, 10000);//t1线程和t2线程共享一个账户Thread t1 = new Thread(new MyRunnable(account));t1.setName("t1");Thread t2 = new Thread(new MyRunnable(account));t2.setName("t2");//启动线程t1.start();t2.start();}
}class MyRunnable implements Runnable{private Account account;public MyRunnable(Account account) {this.account = account;}@Overridepublic void run() {account.withDraw(1000);}
}
class Account{private int actNo; //账户编号private double balance; //账户余额public Account(int actNo, double balance) {this.actNo = actNo;this.balance = balance;}public int getActNo() {return actNo;}public void setActNo(int actNo) {this.actNo = actNo;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/*** 取款* @param money 取款额度*/public synchronized void withDraw(double money) { //直接在方法上加上synchronized关键字//为了演示出多线程并发带来的安全问题,这里将取款分为两步//1. 获取余额double before = this.getBalance();System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前账户" + this.getActNo() + "的账户余额为" + before);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//2. 取款后修改余额this.setBalance(before - money);System.out.println(Thread.currentThread().getName() + "线程取款成功,当前账户" + this.getActNo() + "的账户余额为" + this.getBalance());}
}

运行结果同上。

6.3 关于线程同步的面试题

分析以下程序,m2方法在执行的时候,需要等待m1方法的结束吗?

6.3.1 版本1

package threadtest.thread15;public class ThreadTest15 {public static void main(String[] args) {MyClass mc = new MyClass();Thread t1 = new Thread(new MyRunnable(mc));Thread t2 = new Thread(new MyRunnable(mc));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
class MyRunnable implements Runnable{private MyClass mc;public MyRunnable(MyClass mc){this.mc = mc;}@Overridepublic void run() {if("t1".equals(Thread.currentThread().getName())){mc.m1();}if("t2".equals(Thread.currentThread().getName())){mc.m2();}}
}
class MyClass{public synchronized void m1(){ //同步方法System.out.println("m1 begin");try {Thread.sleep(1000*5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 over");}public void m2(){ //非同步方法System.out.println("m2 begin");System.out.println("m2 over");}
}

运行结果:
在这里插入图片描述
不需要等待。
线程t1执行的是同步方法m1(),需要获取对象锁;
线程t2执行的是非同步方法m2(),并不需要获取对象锁。所以线程t2不需要等待线程t1。

6.3.2 版本2

package threadtest.thread15;public class ThreadTest15 {public static void main(String[] args) {MyClass mc = new MyClass();Thread t1 = new Thread(new MyRunnable(mc));Thread t2 = new Thread(new MyRunnable(mc));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
class MyRunnable implements Runnable{private MyClass mc;public MyRunnable(MyClass mc){this.mc = mc;}@Overridepublic void run() {if("t1".equals(Thread.currentThread().getName())){mc.m1();}if("t2".equals(Thread.currentThread().getName())){mc.m2();}}
}
class MyClass{public synchronized void m1(){ //同步方法System.out.println("m1 begin");try {Thread.sleep(1000*5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 over");}public synchronized void m2(){ //同步方法System.out.println("m2 begin");System.out.println("m2 over");}
}

运行结果:
在这里插入图片描述

需要等待。
由于线程t1执行的是同步方法m1(),需要获取对象锁;
线程t2执行的也是同步方法m2(),也需要获取对象锁。而线程t1和线程t2共享同一个对象,所以线程t2需要等待线程t1。

6.3.3 版本3

package threadtest.thread15;public class ThreadTest15 {public static void main(String[] args) {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new Thread(new MyRunnable(mc1));Thread t2 = new Thread(new MyRunnable(mc2));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
class MyRunnable implements Runnable{private MyClass mc;public MyRunnable(MyClass mc){this.mc = mc;}@Overridepublic void run() {if("t1".equals(Thread.currentThread().getName())){mc.m1();}if("t2".equals(Thread.currentThread().getName())){mc.m2();}}
}
class MyClass{public synchronized void m1(){ //同步方法System.out.println("m1 begin");try {Thread.sleep(1000*5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 over");}public synchronized void m2(){ //同步方法System.out.println("m2 begin");System.out.println("m2 over");}
}

运行结果:
在这里插入图片描述
不需要等待。
虽然m1和m2方法都是同步方法,但是线程t1和线程t2并没有共享对象,其分别调用不同对象,各自占用各自对象的对象锁即可,不存在需要同一对象锁的问题。

6.3.4 版本4

package threadtest.thread15;public class ThreadTest15 {public static void main(String[] args) {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new Thread(new MyRunnable(mc1));Thread t2 = new Thread(new MyRunnable(mc2));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
class MyRunnable implements Runnable{private MyClass mc;public MyRunnable(MyClass mc){this.mc = mc;}@Overridepublic void run() {if("t1".equals(Thread.currentThread().getName())){mc.m1();}if("t2".equals(Thread.currentThread().getName())){mc.m2();}}
}
class MyClass{public static synchronized void m1(){ //静态同步方法System.out.println("m1 begin");try {Thread.sleep(1000*5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 over");}public static synchronized void m2(){ //静态同步方法System.out.println("m2 begin");System.out.println("m2 over");}
}

运行结果:
在这里插入图片描述

需要等待。
因为此时m1和m2方法都是静态方法,且又都加上了synchronized关键字,此时线程同步时会找类锁。类锁是对于一个类来说只有一把锁,不管创建了多少个对象,类锁都只有一把。

总结:
在静态方法上添加synchronized关键字,实际上是为了保证静态变量的安全;
在实例方法上添加synchronized关键字,实际上是为了保证实例变量的安全。

7.死锁

当有多个线程同时共享多个对象时可能会发生死锁问题:

示例代码:

package threadtest.thread16;public class ThreadTest16 {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();Thread t1 = new Thread(new MyRunnable(o1,o2));Thread t2 = new Thread(new MyRunnable(o1,o2));t1.setName("t1");t2.setName("t2");t1.start();t2.start();}
}class MyRunnable implements Runnable{private Object o1;private Object o2;public MyRunnable(Object o1, Object o2) {this.o1 = o1;this.o2 = o2;}@Overridepublic void run() {if(Thread.currentThread().getName().equals("t1")){synchronized (o1){try {Thread.sleep(1000); //为了产生死锁,这里设置睡眠} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){}}}if(Thread.currentThread().getName().equals("t2")){synchronized (o2){try {Thread.sleep(1000); //为了产生死锁,这里设置睡眠} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}}
}

以上代码线程t1先占用o1对象的对象锁,线程t2先占用o2对象的对象锁,而后线程t1又需要寻找o2对象的对象锁,线程t2又需要寻找o1对象的对象锁,由于所需的对象锁分别被对方占用,所以只能陷入无休止的互相等待中。造成死锁。

运行结果(永远无法结束):
在这里插入图片描述
死锁容易发生在synchronized嵌套中,所以对synchronized要慎重使用。

7.1 多线程卖票问题

3个线程同时卖票(不排队):

package threadtest.thread17;public class ThreadTest17 {public static void main(String[] args) {//创建一个对象,让多个线程共享一个对象Runnable r = new MyRunnable();//创建3个线程,模拟3个售票窗口Thread t1 = new Thread(r);t1.setName("1");Thread t2 = new Thread(r);t2.setName("2");Thread t3 = new Thread(r);t3.setName("3");t1.start();t2.start();t3.start();}
}class MyRunnable implements Runnable{private int ticketNum = 100;@Overridepublic void run() {while (true) {if(ticketNum <= 0){System.out.println("票已售完!");break; //停止售票}//票还有try {//出票等待时间Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口" + Thread.currentThread().getName() + "成功售出一张票,当前剩余票数:" + (--ticketNum));}}  
}

运行结果(出现数据问题,剩余票数出现负数):
在这里插入图片描述

排队卖票,即使用线程同步机制:

package threadtest.thread17;public class ThreadTest17 {public static void main(String[] args) {//创建一个对象,让多个线程共享一个对象Runnable r = new MyRunnable();//创建3个线程,模拟3个售票窗口Thread t1 = new Thread(r);t1.setName("1");Thread t2 = new Thread(r);t2.setName("2");Thread t3 = new Thread(r);t3.setName("3");t1.start();t2.start();t3.start();}
}class MyRunnable implements Runnable{private int ticketNum = 100;@Overridepublic void run() {synchronized (this) { //共享对象就是当前对象while (true) {if(ticketNum <= 0){System.out.println("票已售完!");break; //停止售票}//票还有try {//出票等待时间Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口" + Thread.currentThread().getName() + "成功售出一张票,当前剩余票数:" + (--ticketNum));}}}
}

运行结果:
在这里插入图片描述

8.线程通信

线程通信的三个方法:wait()、notify()、notifyAll()

  • wait(): 线程执行该方法后,进入等待状态,并且释放对象锁。
  • notify(): 唤醒优先级最高的那个等待状态的线程。【优先级相同的,随机选一个】。被唤醒的线程从当初wait()的位置继续执行。
  • notifyAll(): 唤醒所有wait()的线程。

例如:对于多线程共享的对象obj,调用了obj.wait()之后,在obj对象上活跃的所有线程都进入无期限等待,直到调用了该共享对象的notify()方法进行了唤醒。唤醒后,会接着上一次调用wait()方法的位置继续向下执行。

需要注意的:

  • 这三个方法都是Object类的方法。
  • 以上三个方法在使用时,必须在同步代码块中或同步方法中。
  • 调用这三个方法的对象必须是共享的锁对象。

8.1 wait()和sleep的区别?

相同点: 都会阻塞。
不同点:

  • wait是Object类的实例方法。sleep是Thread的静态方法。
  • wait只能用在同步代码块或同步方法中。sleep随意。
  • wait方法执行会释放对象锁。sleep不会。
  • wait结束时机是notify唤醒,或达到指定时间。sleep结束时机是到达指定时间。

wait()方法有三个重载方法:

  • wait():调用此方法,线程进入“等待状态”;
  • wait(毫秒):调用此方法,线程进入“超时等待状态”;
  • wait(毫秒, 纳秒):调用此方法,线程进入“超时等待状态”。

8.2 两个线程交替输出

要求创建两个线程交替输出1-100如下:

t1–>1
t2–>2
t1–>3
t2–>4
t1–>5
t2–>6

直到100

需要交替输出则需要进行线程间的通信,代码如下:

package threadtest.thread18;public class ThreadTest18 {public static void main(String[] args) {Runnable r = new MyRunnable();Thread t1 = new Thread(r);t1.setName("t1");Thread t2 = new Thread(r);t2.setName("t2");t1.start();t2.start();}
}class MyRunnable implements Runnable{private int count = 0;@Overridepublic void run() {synchronized (this) {while (true) {//前面t1释放锁后,t2线程开始占用对象锁,开始执行这里同步代码块的内容,//这里需要记得唤醒t1线程//t2线程执行过程中把t1唤醒了,但是由于t2仍然占用对象锁,所以即使t1醒了,也不会往下执行,需要等到t2释放对象锁this.notify();if (count >= 100) {break;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "-->" + (++count));try {//让其中一个线程等待,这个等待的线程可能是t1,也可能是t2//假设目前执行的是t1线程,则t1线程释放对象锁,进入无限期的等待,直到notify()唤醒this.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}
}

8.3 三个线程交替输出

要求创建三个线程交替输出A、B、C如下:

t1–>A
t2–>B
t3–>C
t1–>A
t2–>B
t3–>C

按照以上输出10遍(以上是2遍的示例)

代码如下:

package threadtest.thread19;public class ThreadTest19 {// 三个静态输出标记值,初始值表示第一次输出的时候,t1先输出static boolean t1Output = true;static boolean t2Output = false;static boolean t3Output = false;public static void main(String[] args) {//共享对象(t1、t2、t3线程共享一个对象)Object lock = new Object();//t1线程,负责输出AThread t1 = new Thread(new Runnable() {@Overridepublic void run() {//同步代码块synchronized (lock){for (int i = 0; i < 10; i++) {while (!t1Output){ //只要不是t1输出,t1Output为false,则让线程进入等待状态try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}//轮到t1输出了,并且t1线程被唤醒了System.out.println(Thread.currentThread().getName()+"-->A");//修改布尔标记的值t1Output = false;t2Output = true;t3Output = false;//唤醒所有线程lock.notifyAll();}}}});t1.setName("t1");t1.start();//t2线程,负责输出BThread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock){for (int i = 0; i < 10; i++) {while (!t2Output){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}//轮到t2输出了,并且t2线程被唤醒了System.out.println(Thread.currentThread().getName()+"-->B");//修改布尔标记的值t1Output = false;t2Output = false;t3Output = true;//唤醒所有线程lock.notifyAll();}}}});t2.setName("t2");t2.start();//t3线程,负责输出CThread t3 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock){for (int i = 0; i < 10; i++) {while (!t3Output){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}//轮到t3输出了,并且t3线程被唤醒了System.out.println(Thread.currentThread().getName()+"-->C");//修改布尔标记的值t1Output = true;t2Output = false;t3Output = false;//唤醒所有线程lock.notifyAll();}}}});t3.setName("t3");t3.start();}
}

8.4 线程通信-生产者和消费者模式

线程通信可以实现生产者和消费者均衡:
在这里插入图片描述

9.线程生命周期的回顾与补充

在这里插入图片描述
完善线程的生命周期图:
在这里插入图片描述

10.单例模式

10.1 饿汉式单例模式

package singleton;/*** 饿汉式单例模式*/
public class Singleton {private static Singleton singleton = new Singleton();private Singleton(){}public static Singleton getSingleton(){return singleton;}public static void main(String[] args) {Singleton singleton1 = Singleton.getSingleton();Singleton singleton2 = Singleton.getSingleton();System.out.println(singleton1 == singleton2); //true}
}

10.2 懒汉式单例模式

package singleton;/*** 懒汉式单例模式*/
public class Singleton {private static Singleton singleton;private Singleton(){}public static Singleton getSingleton(){if(singleton == null){singleton = new Singleton();}return singleton;}public static void main(String[] args) {Singleton singleton1 = Singleton.getSingleton();Singleton singleton2 = Singleton.getSingleton();System.out.println(singleton1 == singleton2);  //true}
}

10.3 懒汉式单例模式可能会造成线程安全问题

package threadtest.thread20;public class ThreadTest20 {//静态变量private static Singleton s1;private static Singleton s2;public static void main(String[] args) {//创建线程t1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {s1 = Singleton.getSingleton();}});//创建线程t2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {s2 = Singleton.getSingleton();}});//启动线程t1.start();t2.start();//保证t1、t2线程在main方法结束前执行(需要线程先start才能起作用)try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}//判断两个Singleton对象是否一样。这里为什么不一样啊System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}
}/*** 懒汉式单例模式*/
class Singleton {private static Singleton singleton;private Singleton(){System.out.println("构造方法执行了!");}public static Singleton getSingleton(){if(singleton == null){singleton = new Singleton();}return singleton;}
}

运行结果(出现构造方法执行2次,创建了2个对象):
在这里插入图片描述
解决方案1:同步方法

package threadtest.thread20;public class ThreadTest20 {//静态变量private static Singleton s1;private static Singleton s2;public static void main(String[] args) {//创建线程t1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {s1 = Singleton.getSingleton();}});//创建线程t2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {s2 = Singleton.getSingleton();}});//启动线程t1.start();t2.start();//保证t1、t2线程在main方法结束前执行(需要线程先start才能起作用)try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}//判断两个Singleton对象是否一样。这里为什么不一样啊System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}
}/*** 懒汉式单例模式*/
class Singleton {private static Singleton singleton;private Singleton(){System.out.println("构造方法执行了!");}//第一种方法(同步方法):在方法声明处加上synchronized关键字,由于又是静态方法,让线程排队执行,去找类锁public static synchronized Singleton getSingleton(){if(singleton == null){singleton = new Singleton();}return singleton;}
}

运行结果:
在这里插入图片描述

解决方案2:同步代码块

package threadtest.thread20;public class ThreadTest20 {//静态变量private static Singleton s1;private static Singleton s2;public static void main(String[] args) {//创建线程t1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {s1 = Singleton.getSingleton();}});//创建线程t2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {s2 = Singleton.getSingleton();}});//启动线程t1.start();t2.start();//保证t1、t2线程在main方法结束前执行(需要线程先start才能起作用)try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}//判断两个Singleton对象是否一样。这里为什么不一样啊System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}
}/*** 懒汉式单例模式*/
class Singleton {private static Singleton singleton;private Singleton(){System.out.println("构造方法执行了!");}//第二种方法(同步代码块):方法内部设置同步代码块,让线程排队执行,也去找类锁public static Singleton getSingleton(){synchronized (Singleton.class){ //Singleton.class为反射机制中的内容,获取Singleton类if(singleton == null){singleton = new Singleton();}}return singleton;}
}

运行结果:
在这里插入图片描述

解决方案3:针对方案2进行优化,提高效率

package threadtest.thread20;public class ThreadTest20 {//静态变量private static Singleton s1;private static Singleton s2;public static void main(String[] args) {//创建线程t1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {s1 = Singleton.getSingleton();}});//创建线程t2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {s2 = Singleton.getSingleton();}});//启动线程t1.start();t2.start();//保证t1、t2线程在main方法结束前执行(需要线程先start才能起作用)try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}//判断两个Singleton对象是否一样。这里为什么不一样啊System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}
}/*** 懒汉式单例模式*/
class Singleton {private static Singleton singleton;private Singleton(){System.out.println("构造方法执行了!");}//第二种方法(同步代码块+外面再嵌套一个if语句,减少一次找类锁的时间)public static Singleton getSingleton(){if(singleton== null) {synchronized (Singleton.class) { //Singleton.class为反射机制中的内容,获取Singleton类if (singleton == null) {singleton = new Singleton();}}}return singleton;}
}

解决方案4:使用可重入锁ReentrantLock(看下面介绍)

11.可重入锁ReentrantLock

Java还有一个Lock接口,从JDK5开始引入,Lock接口下有一个实现类:可重入锁(ReentrantLock),其也可以实现线程安全,且比synchronized更推荐使用,因为其更加灵活,可以更细粒度地控制同步代码,但是一定要记住解锁!!!!

注意:要想使用ReentrantLock达到线程安全,假设要让t1、t2、t3线程同步,就需要让t1、t2、t3共享同一个ReentrantLock对象。

语法:
保证多个线程共享一个ReentrantLock对象,比如如下的:private static final ReentrantLock lock = new ReentrantLock();,然后在需要同步的代码块前面加锁,即lock.lock();,并在后面解锁,即lock.unlock();

上述懒汉式单例模式保证线程安全:

package threadtest.thread21;import java.util.concurrent.locks.ReentrantLock;public class ThreadTest21 {//静态变量private static Singleton s1;private static Singleton s2;public static void main(String[] args) {//创建线程t1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {s1 = Singleton.getSingleton();}});//创建线程t2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {s2 = Singleton.getSingleton();}});//启动线程t1.start();t2.start();//保证t1、t2线程在main方法结束前执行(需要线程先start才能起作用)try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}//判断两个Singleton对象是否一样。这里为什么不一样啊System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}
}/*** 懒汉式单例模式*/
class Singleton {private static Singleton singleton;private Singleton(){System.out.println("构造方法执行了!");}//创建共享锁对象,需要设置为静态的,才是共享的!private static final ReentrantLock lock = new ReentrantLock();//第四种方法:使用可重入锁ReentrantLockpublic static Singleton getSingleton(){try {//加锁lock.lock();if (singleton == null) {singleton = new Singleton();}} finally {//解锁(需要100%保证解锁,所以需要使用finally)lock.unlock();}return singleton;}
}

运行结果:
在这里插入图片描述

12.实现线程的第三种方式:实现Callable接口

实现线程的第三种方式:实现Callable接口。这种方式实现线程可以获取到线程的返回值。

package threadtest.thread22;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;/*** 实现线程的第三种方式:实现Callable接口,覆写call()方法*/
public class ThreadTest22 {public static void main(String[] args) {//创建“未来任务”对象,设置的泛型为线程的返回类型FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception { //与run()方法不一样的是,call方法可以抛出异常//处理业务Thread.sleep(5000);return 1;}});//创建线程对象Thread t = new Thread(task);t.setName("t");//启动线程t.start();try {//获取“未来任务”线程的返回值//会阻塞当前线程,等待“未来任务”结束并返回//拿到返回值,当前线程的阻塞才会解除,继续执行。(这里则表现为会等待5秒)Integer i = task.get();System.out.println("t线程返回值:" + i);} catch(Exception e){e.printStackTrace();}}
}

运行结果:在这里插入图片描述

13.实现线程的第四种方式:使用线程池

线程池本质上就是一个缓存(cache)。一般都是服务器在启动的时候,初始化线程池,即服务器在启动的时候创建多个线程对象,直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。

package threadtest.thread23;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadTest23 {public static void main(String[] args) {//创建一个线程池对象(线程池中有3个线程)ExecutorService executorService = Executors.newFixedThreadPool(3);//将任务交给线程池(无需触碰到这个线程对象,只需要将要处理的任务交给线程池即可)executorService.submit(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "-->" + i);}}});//最后记得关闭线程池executorService.shutdown();}
}

运行结果(这里只用到了线程池中的一个线程):在这里插入图片描述

相关文章:

JavaSE-11笔记【多线程2(+2024新)】

文章目录 6.线程安全6.1 线程安全问题6.2 线程同步机制6.3 关于线程同步的面试题6.3.1 版本16.3.2 版本26.3.3 版本36.3.4 版本4 7.死锁7.1 多线程卖票问题 8.线程通信8.1 wait()和sleep的区别&#xff1f;8.2 两个线程交替输出8.3 三个线程交替输出8.4 线程通信-生产者和消费者…...

WebKit是什么?

WebKit是一个开源的浏览器引擎&#xff0c;它用于呈现网页内容在许多现代浏览器中&#xff0c;包括Safari浏览器、iOS内置浏览器、以及一些其他浏览器如Google Chrome的早期版本。以下是一些关于WebKit的重要信息&#xff1a; 起源和发展&#xff1a;WebKit最初是由苹果公司为其…...

谷歌(Google)历年编程真题——接雨水

谷歌历年面试真题——数组和字符串系列真题练习。 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;…...

golang 归并回源策略

前言 下面是我根据业务需求画了一个架构图&#xff0c;没有特别之处&#xff0c;很普通&#xff0c;都是我们常见的中间件&#xff0c;都是一些幂等性GET 请求。有一个地方很有意思&#xff0c;从service 分别有10000 qps 请求到Redis&#xff0c;并且它们的key 是一样的。这样…...

【漏洞复现】可视化融合指挥调度平台 dispatch接口处存在任意文件上传漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…...

最讨厌这种字符串问题了!!

题目&#xff1a;洛谷P1957口算练习题 题目大意描述&#xff1a; 第一行输入一个整数表示接下来要进行多少次运算&#xff0c;接下来每行输入一个字母c和两个数字x,y&#xff08;输入的字母为a/b/c,分别表示要进行&#xff0c;-&#xff0c;*运算&#xff09;或者就输入两个数…...

B-名牌赌王(本人遇到的题,做个笔记)

题解&#xff1a; #include <iostream> #include <queue> //需要用小根堆的优先队列 #include <unordered_map> //用无序映射 using namespace std; bool pai() {int n, m;cin >> n >> m; priority_queue<int, vector<int>, gr…...

博客评论回复03

接着之前写的&#xff0c;之前返回的数据集按道理来说渲染出来还是丑丑的&#xff0c;因此这次我看着抖音的评论样子&#xff0c;自己瞎写了一通&#xff0c;不过也算是模仿出来了虽然肯定没有抖音写的好。 类似与前面几章写的表结构 首先看看抖音评论区是怎么样的&#xff1f…...

【【萌新的学习之Numpy数组的使用】】

萌新的学习之Numpy数组的使用 先记录一下之前的关于函数的设计 通过创造类的形式 复习完毕之后介绍numpy数组的使用 #整数型数组遇到除法 &#xff08;即便是除以整数&#xff09; 不同维度的数组之间 从外形上的本质区别 一维数组用1层中括号 二维数组用2层中括号 三维数…...

RabbitMQ3.13.x之七_RabbitMQ消息队列模型

RabbitMQ3.13.x之七_RabbitMQ消息队列模型 文章目录 RabbitMQ3.13.x之七_RabbitMQ消息队列模型1. RabbitMQ消息队列模型1. 简单队列2. Work Queues(工作队列)3. Publish/Subscribe(发布/订阅)4. Routing(路由)5. Topics(主题)6. RPC(远程过程调用)7. Publisher Confirms(发布者…...

Android JNI 调用第三方SO

最近一个项目使用了Go 编译了一个so库&#xff0c;但是这个so里面还需要使用第三方so库pdfium, 首先在Android工程把2个so库都放好 在jni中只能使用dlopen方式&#xff0c;其他的使用函数指针的方式来调用&#xff0c;和windows dll类似&#xff0c;不然虽然编译过了但是会崩溃…...

Vid2seq

Vid2Seq 应该是目前为止,个人最中意得一篇能够实际解决对一段视频进行粗略理解得paper了。个人认为它能够真正能解决视频理解是因为它是对一个模型整体做了训练,而不仅仅是通过visual encoders(e.g BLIP/CLIP/…)和 其它multi modal 的encoder直接过了个projection,做一个…...

Opencv人机交互界面设置

Opencv人机交互界面设置 以下是一些常见的OpenCV人机交互界面设置&#xff1a; 窗口交互 显示窗口&#xff1a;可以使用cv2.imshow()函数在屏幕上显示图像。例如&#xff0c;要显示名为“image”的图像&#xff0c;可以使用以下代码&#xff1a; import cv2img cv2.imread…...

蓝桥杯算法心得——字典树考试(贡献度+前缀和)

大家好&#xff0c;我是晴天学长&#xff0c;贡献度的题&#xff0c;找到技巧非常重要&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .字典树考试 字典树考试 问题描述 蓝桥学院最近教学了字典树这一数…...

Linux下Qt生成程序崩溃文件

文章目录 1.背景2.Qt编译生成程序2.1.profile模式的本质 3.执行程序&#xff0c;得到core文件4.代码定位4.1.直接使用gdb4.2.使用QtCreator 5.总结6.题外话6.1.profile模式和debug模式的区别 1.背景 在使用Qt时&#xff0c;假如在windows&#xff0c;当软件崩溃时&#xff0c;…...

Go语言中测试和性能

1. 测试:软件开发最重要的方面 测试软件程序可能是软件开发人员能够做的最重要的事情。通过测试代码的功能,开发人员能够在很大程度上确定程序是有效的。另外,每次修改代码后,开发人员都可运行测试,确认没有引入Bug和衰退。通过测试软件,还能够让软件工程师确认程序按期望…...

回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测

回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测 目录 回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab基于CPO-GPR基于冠豪猪算法优化高斯…...

python 日期字符串转换为指定格式的日期

在Python编程中&#xff0c;日期处理是一个常见的任务。我们经常需要将日期字符串转换为Python的日期对象&#xff0c;以便进行日期的计算、比较或其他操作。同时&#xff0c;为了满足不同的需求&#xff0c;我们还需要将日期对象转换为指定格式的日期字符串。本文将详细介绍如…...

day03-Docker

1.初识 Docker 1.1.什么是 Docker 1.1.1.应用部署的环境问题 大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题&#xff1a; 依赖关系复杂&#xff0c;容易出现兼容性问题开发、测试、生产环境有差异 例如一个项目中&#xff0c;部署时需要依…...

C语言函数实现冒泡排序

前言 今天我们来看看怎么使用函数的方式实现冒泡排序吧&#xff0c;我们以一个数组为例arr[] {9,8,7,6,5,4,3,2,1,0},我们将这个数组通过冒泡排序的方式让他变为升序吧。 代码实现 #include<stdio.h> void bubble_sort(int arr[], int sz) {int i 0;for (i 0;i < s…...

区间概率预测python|QR-CNN-BiLSTM+KDE分位数-卷积-双向长短期记忆神经网络-时间序列区间概率预测+核密度估计

区间预测python|QR-CNN-BiLSTMKDE分位数-卷积-双向长短期记忆神经网络-核密度估计-回归时间序列区间预测 模型输出展示&#xff1a; (图中是只设置了20次迭代的预测结果&#xff0c;宽度较宽&#xff0c;可自行修改迭代参数&#xff0c;获取更窄的预测区间&#xff09; 注&am…...

Java 分支结构 - if…else/switch

顺序结构只能顺序执行&#xff0c;不能进行判断和选择&#xff0c;因此需要分支结构。 Java有两种分支结构&#xff1a; if语句switch语句 if语句 一个if语句包含一个布尔表达式和一条或多条语句。 语法 If 语句的用语法如下&#xff1a; if(布尔表达式) {//如果布尔表达…...

【Unity每日一记】如何从0到1将特效图集制作成一个特效

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…...

磁力链接的示例与解释

磁力链接&#xff08;Magnet URI scheme&#xff09;是一种特殊类型的统一资源标识符&#xff08;URI&#xff09;&#xff0c;它包含了通过特定散列函数&#xff08;如SHA-1&#xff09;得到的文件内容的散列值&#xff0c;而不是基于位置或名称的引用。这使得磁力链接成为在分…...

云存储中常用的相同子策略的高效、安全的基于属性的访问控制的论文阅读

参考文献为2022年发表的Efficient and Secure Attribute-Based Access Control With Identical Sub-Policies Frequently Used in Cloud Storage 动机 ABE是实现在云存储中一种很好的访问控制手段,但是其本身的计算开销导致在实际场景中应用收到限制。本论文研究了一种LSSS矩…...

JVM高级篇之GC

文章目录 版权声明垃圾回收器的技术演进ShenandoahShenandoah GC体验Shenandoah GC循环过程 ZGCZGC简介ZGC的版本更迭ZGC体验&使用ZGC的参数设置ZGC的调优 版权声明 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明&#xff0c;所有版权属于黑马…...

第十四届蓝桥杯省赛大学C组(C/C++)三国游戏

原题链接&#xff1a;三国游戏 小蓝正在玩一款游戏。 游戏中魏蜀吴三个国家各自拥有一定数量的士兵 X,Y,Z&#xff08;一开始可以认为都为 0&#xff09;。 游戏有 n 个可能会发生的事件&#xff0c;每个事件之间相互独立且最多只会发生一次&#xff0c;当第 i 个事件发生时…...

java之static详细总结

static也叫静态&#xff0c;可以修饰成员变量、成员方法。 成员变量 按照有无static分为两种&#xff1a; 类变量&#xff1a;static修饰&#xff0c;属于类&#xff0c;与类一起加载一次&#xff0c;在内存中只有一份&#xff0c;会被类的全部对象共享实例变量&#xff08;…...

RabbitMQ3.13.x之六_RabbitMQ使用场景

RabbitMQ3.13.x之六_RabbitMQ使用场景 文章目录 RabbitMQ3.13.x之六_RabbitMQ使用场景1. 为什么选择 RabbitMQ&#xff1f;1. 可互操作2. 灵活3. 可靠 2. 常见用户案例1. 服务解耦2. 远程过程调用3. 流处理4. 物联网 1. 为什么选择 RabbitMQ&#xff1f; RabbitMQ 是一个可靠且…...

C++ 类和对象(初篇)

类的引入 C语言中&#xff0c;结构体中只能定义变量&#xff0c;在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数。 而为了区分C和C我们将结构体重新命名成class去定义 类的定义 标准格式&#xff1a; class className {// 类体&#xff1a;由成员函…...

珠海网站定制开发/软文是什么东西

一、数据结构与算法基础 说一下几种常见的排序算法和分别的复杂度。 用Java写一个冒泡排序算法 描述一下链式存储结构。 如何遍历一棵二叉树&#xff1f; 倒排一个LinkedList。 用Java写一个递归遍历目录下面的所有文件。 二、Java基础 接口与抽象类的区别&#xff1f;…...

自己做免费手机网站吗/仁茂网络seo

前言 对于drop、truncate和delete&#xff0c;虽然简单&#xff0c;但是真要使用或者面试时候问到还是需要有一定的总结&#xff0c;自己也比较懒&#xff0c;前面很多人总结过了&#xff0c;但是并不完善&#xff0c;因此参看多篇文章之后进行一个总结。在此之前先简单了解下…...

做网站公示/上海公关公司

通俗易懂的斯特林数介绍定义第一类斯特林数第二类斯特林数性质通项递推第一类斯特林数第二类斯特林数特殊值第一类斯特林数第二类斯特林数快速幂&#xff1f;&#xff1f;&#xff1f;斯特林反演定义 第一类斯特林数 将nnn个元素分成mmm个无标号的轮换[nm]\begin{bmatrix}n\\…...

wordpress cnzz 插件/写手接单平台

1、mysql导出 ysqldump -u root -p BackManagerment > myback.sql 2、mysql导入 sudo mysql -u root //显示所有数据库 show databases; //创建数据库设置utf8 create database DeveloperTools default character set utf8; //导入数据库 source my back.sql...

广南网站建设/怎样在百度上发布自己的信息

买了这本书&#xff0c;想着边看边整理一下&#xff0c;可以方便以后查阅。分享出来&#xff0c;大家都可以看看&#xff01;第3章 使用MySQL 使用数据库 —— USE 数据库名称了解数据库 —— SHOW 数据库名称查看数据库中的表 —— SHOW TABLES 查看表中列的消息 —— SHOW CO…...

web前端是网页设计吗/谷歌seo软件

详细步骤&#xff1a; 1、mysql -uroot -p 输完密码处回车即可 mysql -uroot -p 2、查看当前安装的mysql版本 rpm -qa | grep MySQL ps -ef | grep mysql 3、停止mysql服务 service mysql stop 4、进入到skip-grant-tables模式&#xff0c;注意执行完后需要打开另外一个窗口…...