根据尚硅谷的视频做的JUC学习笔记
一、线程的生命周期
一个完整的生命周期通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
二、线程的同步
线程同步的提出主要是为了解决线程的安全问题。例如,当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误;而解决办法就是,对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
同步机制
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized(同步监视器){
需要被同步的代码
}
说明:
1.操作共享数据的代码,即为需要被同步的代码。
2.共享数据:多个线程共同操作的变量。比如窗口卖票案例中的ticket就是共享数据。
3.同步监视器:俗称“锁”。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
补充:1.在实现Runnable接口创建多线程的方式中,我们可以考虑使用
this
充当同步监视器。 2.在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
class Window2 implements Runnable {
private static int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票!");
ticket--;
} else
break;
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Window2 w = new Window2();
Thread w1 = new Thread(w);
Thread w2 = new Thread(w);
Thread w3 = new Thread(w);
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
使用同步方法解决实现Runnable接口的线程安全问题:
//使用同步方法解决实现Runnable接口的线程安全问题
class Window3 implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//定义同步方法
private synchronized void show() {//同步监视器:this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第 " + ticket + " 张票!");
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread w1 = new Thread(w);
Thread w2 = new Thread(w);
Thread w3 = new Thread(w);
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
使用同步方法解决继承Thread类的方式中的线程安全问题
//使用同步方法解决继承Thread类的方式中的线程安全问题
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//private synchronized void show() { 这种方式是错误的
private static synchronized void show() { //同步监视器:Window4.class
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第 " + ticket + " 张票!");
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.setName("窗口 1 ");
w2.setName("窗口 2 ");
w3.setName("窗口 3 ");
w1.start();
w2.start();
w3.start();
}
}
关于同步方法的总结:
- 1.同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。
- 2.非静态的同步方法,同步监视器是this。静态的同步方法,同步监视器是当前类的本身。
同步的方式,解决了现成的安全问题是好处。但是,操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低,这即是同步的局限性。
单例设计模式之懒汉式(线程安全)
使用同步机制将单例模式中的懒汉式改写为线程安全的
class Bank {
private Bank() {
}
private static Bank instance = null;
//public static synchronized Bank getInstance() {
public static Bank getInstance() {
//方式一:效率稍差
/*synchronized (Bank.class) {
if (instance == null)
instance = new Bank();
return instance;
}*/
//方式二:效率稍高
if (instance == null) {
synchronized (Bank.class) {
if (instance == null)
instance = new Bank();
return instance;
}
}
return instance;
}
}
三、线程的死锁问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:专门的算法、原则;尽量减少同步资源的定义;尽量避免嵌套同步。
线程死锁:
//演示线程死锁问题
public class ThreadTest4 {
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (sb1){
sb1.append("a");
sb2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb2){
sb1.append("b");
sb2.append("2");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (sb2){
sb1.append("c");
sb2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb1){
sb1.append("d");
sb2.append("4");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}).start();
}
}
出现死锁:
四、Lock锁
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象。ReentrantLock
类实现了 Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁。
import java.util.concurrent.locks.ReentrantLock;
//解决线程安全问题的方式三:Lock锁
class Window5 implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法:lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票!");
ticket--;
} else
break;
} finally {
//3.调用解锁的方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window5 w = new Window5();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
synchronized与Lock的对比
- Lock是显示锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,
JVM
将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。优先使用顺序:Lock–>同步代码块(已经进入了方法体,分配了相应资源)–>同步方法(在方法体之外)。
import java.util.concurrent.locks.ReentrantLock;
/**
* 练习:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
* 分析:是否是多线程问题? 是,因为有两个账户
* 是否有共享数据? 有,账户余额就是
* 是否有线程安全问题? 有
* 需要考虑如何解决线程安全问题? 同步机制:三种方式(同步代码块、同步方法、Lock锁)
*/
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱的方法
//public synchronized void deposit(double amt) {
public void deposit(double amt) {
if (amt > 0) {
balance += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存钱成功,账户余额为:" + balance);
}
}
}
class Customer implements Runnable {
private Account account;
public Customer(Account account) {
this.account = account;
}
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
} finally {
lock.unlock();
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
Account account = new Account(0);
Customer c = new Customer(account);
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.setName("客户一");
t2.setName("客户二");
t1.start();
t2.start();
}
}
五、线程的通信
常用方法
wait()
:令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()
或notifyAll()
方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。notify()
:唤醒正在排队等待同步资源的线程中优先级最高者结束等待。notifyAll()
:唤醒正在排队等待资源的所有线程结束等待。
注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException
异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。调用这三个方法的必要条件是:当前线程必须具有该对象的监控权(加锁)。
//线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
sleep()和wait()的异同
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态,并释放同步监视器。
不同点:(1)两个方法的声明位置不同:Thread类中声明sleep(),Object类中声明wait()。(2)调用的要求不同:sleep()可以在任何需要的场景下调用;而wait()必须使用在同步代码块或同步方法中。(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,而wait()会释放锁。