文章目录
- 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 线程安全问题
- 什么情况下需要考虑线程安全问题?
多线程并发的环境下,有共享的数据,且涉及到共享数据的修改操作。
一般情况下:
①局部变量若为基本数据类型,则不存在线程安全问题【在栈中,栈不是共享的】;而若为引用数据类型则另说了。
②实例变量可能存在线程安全问题,实例变量在堆中,堆是多线程共享的。
③静态变量也可能存在安全问题,静态变量在堆中,堆是多线程共享的。
多个线程并发对同一个银行账户进行取款操作时,会有安全问题: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;
}
@Override
public 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;
}
@Override
public 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关键字:
- 在实例方法上添加了synchronized关键字之后,整个方法体就是一个同步代码块。
- 在实例方法上添加了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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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;
@Override
public 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;
@Override
public 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;
@Override
public 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线程,负责输出A
Thread t1 = new Thread(new Runnable() {
@Override
public 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线程,负责输出B
Thread t2 = new Thread(new Runnable() {
@Override
public 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线程,负责输出C
Thread t3 = new Thread(new Runnable() {
@Override
public 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) {
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public 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) {
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public 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) {
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public 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) {
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public 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) {
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public 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();
//第四种方法:使用可重入锁ReentrantLock
public 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>() {
@Override
public 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() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
});
//最后记得关闭线程池
executorService.shutdown();
}
}
运行结果(这里只用到了线程池中的一个线程):