线程同步
当多个线程并行访问一个共享数据时,可能会导致数据的不准确问题。举例如下:
//统计线程访问量类
class Account implements Runnable{
private static int n;
@Override
public void run(){
try{
Thread.currentThread().sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
n++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+n+"位访客");
}
}
public class Test(){
public static void main(String[] args){
Account account = new Account();
Thread t_A = new Thread(account,"线程A");
thread t_B = new Thread(account,"线程B");
t_A.start();
t_B.start();
}
}
结果分析:线程A执行n++,还没有打印访客信息,线程B也执行了n++,所以n=2,两个线程都打印出事第2位访客
线程同步的实现
可以通过synchronized修饰方法来实现线程同步,每个Java对象都有一个内置锁,内置锁会保护用synchronized关键字修饰的方法,要调用该方法必须先获得内置锁,否则处于阻塞状态。
我们对Account类做一下修改
class Account implements Runnable{
private static int n;
@Override
public synchronized void run(){
try{
Thread.currentThread().sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
n++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+n+"位访客");
}
}
再次运行
代码讲解:线程A先到,获取了run()方法的锁,之后线程B到了,此时run()方法已经被锁起来,要调用必须先拿到锁,线程A调用完方法之后才会释放所,所以A先执行,B再执行,看到的就是正确的结果
- 给静态方法加synchronized关键字实现同步
public class SynchronizedTest2 {
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest2.test();
}
});
thread.start();
}
}
public synchronized static void test() {
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
System.out.println("end...");
}
}
如果不加synchronized关键字,start和end不会成对出现,因为在线程thread第一次创建运行时会休眠1000毫秒,这个时间段主线程执行完for循环绰绰有余,静态方法test()也没有加锁,所以会先打印五个start…再打印五个end…
重点:
这里的test()方法是静态方法,如果改为实例方法,则无效,因为线程同步的本质是锁定多个线程所共享的资源,而每个线程都有自己单独的实例方法,相互之间是独立的。实际运行情况是每一个线程都获取自己的锁,然后并行访问,不存在“你运行,我等待”的关系,所以给实例方法添加synchronized关键字修饰并不能实现线程同步
添加synchronized可以同步代码块
public class SynchronizedTest3 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest3 synchronizedTest3 = new SynchronizedTest3();
synchronizedTest3.test();
}
});
thread.start();
}
}
//这里也可以是静态方法
public void test() {
//不可以是synchronized(this)
synchronized(SynchronizedTest3.class) {
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
System.out.println("end...");
}
}
}
实例对象是每个线程独有的,类则是共享的,所以锁定类就可以实现同步,所以不能是(this)
线程安全的单例模式
单例模式是一种常见的软件设计模式,其核心思想是一个类只有一个实例对象。核心是共享实例对象,那么就把实例对象定义为静态
public class TestDemo{
private static TestDemo instance;
private TestDemo(){
System.out.println("TestDemo...");
}
//此处加synchronized就会常见一个对象,否则创建两个,可以从运行结果看出来
public synchronized static TestDemo getInstance(){
if(instance == null){
instance = new TestDemo();
}
return instance;
}
public static void main(String[] args){
new Thread(new Runnable(){
@Override
public void run(){
TestDemo instance = TestDemo.getInstance();
}
}).start();
TestDemo instance2 = TestDemo.getInstance();
}
}
- 若getInstance()方法不加synchronized关键字修饰,就会创建两个实例对象,原因是线程1和线程2是并行访问的,线程1先来判断instancenull是成立的,然后线程1来实例化对象,正在此时,实例化对象的操作还没有完成,线程2来了。先判断 instancnull 是成立的,于是线程2 也执行了实例化对象的操作,所以导致实例化了两个对象
- synchronized可以修饰方法,也可以修饰代码块,下面通过同步代码块的方式来实现单例模式
public class SingletonDemo2 {
private volatile static SingletonDemo2 instance;
private SingletonDemo2() {
System.out.println("SingletonDemo2");
}
public static SingletonDemo2 getInstance() {
if(instance == null) {
synchronized(SingletonDemo2.class) {
if(instance == null) {
instance = new SingletonDemo2();
}
}
}
return instance;
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo2 instance = SingletonDemo2.getInstance();
}
}).start();
SingletonDemo2 instance2 = SingletonDemo2.getInstance();
}
}
代码讲解:这里使用volatile关键字修饰instance,volatile的作用是可以使内存中的数据对线程可见。Java的内存模式:一个线程在访问内存数据时,其实不是拿到该数据本身,而是将该数据复制保存到工作内存中。相当于取出一个副本,对工作内存中的数据进行修改,再保存到内存中,即主内存对线程是不可见的。当线程1拿到锁,并锁定整个类之后,就是李华了instance对象,但此时的instance是工作内存中的数据,还需要讲工作内存中的数据存到住内存中。然而锁定的知识实例化的步骤,保存到主内存的步骤没有加锁。所以工作内存中的instance完成实例化之后,还未更新到主内存就释放了锁,线程2立即获取锁,又从主内存中复制了一份数据,此时的数据还是null。线程2又在工作内存中完成了一次实例化。然后线程1和线程2再将他们各自实例化的数据保存到主内存中
死锁
- 什么是死锁?(举例)
假设10个人围一桌吃饭,但是每个人只有一根筷子,要求必须凑齐一双筷子才可以吃菜。如果把每个人看成一个线程,筷子就是线程要获取的资源,现在每个线程都占用一个资源并且不愿意释放,而且任意一个线程想继续执行就必须获取其他线程的资源,那么所有的线程都处于阻塞状态,程序无法向下执行也无法结束。
- 如何破解死锁?
唯有某个线程愿意做出让步,贡献自己的资源给其他线程使用。获取到资源的线程就可以执行自己垫付业务方法,执行完毕后会释放它锁占有的两个资源。
- 死锁代码举例
class Chopsticks{
}
class DeadLockRunnable implements Runnable{
public int num;
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks();
@Override
public void run(){
if(num == 1){
System.out.println(Thread.currentThread().getName()+"获取到 chopsticks1,等待获取chopsticks2");
synchronized (chopsticks1){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized (chopsticks2){
System.out.println(Thread.currentThread().getName()+"用餐完毕");
}
}
}
if(num == 2){
System.out.println(Thread.currentThread().getName()+"获取到 chopsticks2,等待获取chopsticks1");
synchronized (chopsticks2){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized (chopsticks1){
System.out.println(Thread.currentThread().getName()+"用餐完毕");
}
}
}
}
}
public class DeadLockTest{
public static void main(String[] args){
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1,"张三").start();
new Thread(deadLockRunnable2,"李四").start();
}
}
重入锁
- 什么是重入锁?
重入锁(ReentrantLock)是对synchronized的升级,synchronized是通过 JVM 实现的,ReentrantLock 是通过 JDK 实现的。
- 重入锁有什么特点呢?
重入锁指可以给同一个资源添加多个锁,并且解锁的方式与synchronized也不同,synchronized的锁是线程执行完毕之后会自动释放,ReentrantLock 的锁必须手动释放,可以通过 ReentrantLock 实现访问量统计
class Account3 implements Runnable{
private static int num;
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
reentrantLock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
reentrantLock.unlock();
}
}
public class ReentrantLockTest {
public static void main(String[] args) {
Account3 ac = new Account3();
Thread thread1 = new Thread(ac,"线程1");
Thread thread2 = new Thread(ac,"线程2");
thread1.start();
thread2.start();
}
}
效果与 synchronized 一样,在此基础上可以添加多把锁,只需要多次调用lock()方法即可。
class Account3 implements Runnable{
private static int num;
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
reentrantLock.lock();
reentrantLock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
reentrantLock.unlock();
reentrantLock.unlock();
}
}
重点:我们说过 ReentrantLock 需要手动解锁,如果我们只加锁而不解锁,其他线程将无法获得资源,程序无法继续执行;所以需要注意我们加了几把锁就必须释放几把锁
生产者消费者模式
- 生产者消费者意为在一个生产环境中,生产者和消费者在同一个时间段内共享同一块缓冲区。生产者负责向缓冲区添加数据,消费者负责从缓冲区中取出数据。以生产汉堡和消费汉堡为例来实现生产者消费者模式
//汉堡类
public class Hamburger {
private int id;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public Hamburger(int id) {
this.id = id;
}
@Override
public String toString() {
return "Hamburger [id=" + id + "]";
}
}
//装汉堡的容器类
public class Container {
public Hamburger[] array = new Hamburger[6];
public int index = 0;
//向容器中添加汉堡
public synchronized void push(Hamburger hamburger) {
while(index == array.length) {
try {
this.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
array[index] = hamburger;
index++;
System.out.println("生产了一个汉堡:" + hamburger);
}
//从容器中取出汉堡
public synchronized Hamburger pop() {
while(index == 0) {
try {
this.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
index--;
System.out.println("消费了一个汉堡:" + array[index]);
return array[index];
}
}
//生产者类
public class Producer implements Runnable {
private Container container = null;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for(int i = 0; i < 30; i++) {
Hamburger hamburger = new Hamburger(i);
this.container.push(hamburger);
try {
Thread.currentThread().sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者类
public class Consumer implements Runnable {
private Container container = null;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
for(int i = 0; i < 30; i++) {
this.container.pop();
try {
Thread.currentThread().sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
Container container = new Container();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(producer).start();
new Thread(producer).start();
new Thread(consumer).start();
new Thread(consumer).start();
new Thread(consumer).start();
}
}