小言_互联网的博客

【java多线程】线程同步与锁

413人阅读  评论(0)

java多线程系列:

前言

前一章简单介绍了Thread和Runnable,本章将重点介绍线程同步

多线程的问题

大家都知道多线程可以充分利用计算机资源,提高程序效率,但是稍不留神就会出现难以察觉的问题,而这些问题多数与数据竞争、竞态条件、缓存变量有关。

  • 竞态条件
    竞态条件常见的就是先检查后执行(check-than-act)

    假定n和m是实例变量,一个线程在执行完n==8.0的判断后,被调度器暂停了,另外一个线程获取了执行时间片段,修改了n的值;这时重新获取执行权后m的值不会等于4.0了

     if(n==8.0){
    	m=n / 2.0
    } 
    
  • 数据竞争
    指两个或两个以上的线程并发的访问同一块内存区域,同时至少有一个线程在执行写操作,导致没有获取到预期的结果。通俗理解并发下值未同步,产生多写误差。

    假定线程1调set(10),线程2调get()本应该获取线程1修改后的值,实际没有获取到,这时产生了数据竞争。解决这个问题只需要volatile关键就可以解决,后面讲解volatile

    public class demo{
    	private int a;
    	public void set(int a){
    		this.a = a;
    	}
    	public int get(){
    		return a;
    	}
    }
    
  • 缓存变量

    字面意思是把变量缓存起来,实际是JVM以及操作系统为了提升性能,把变量先放到寄存器或者cpu的缓存区中,对应的每条线程都会有自己的变量拷贝,操作变量时也是操作自己的拷贝,其他线程不太可能看到自己拷贝的变量发生变化。

java如何规避这些问题,是本文重点要说的

synchronized

作用:

  • synchronized旨在保证多个并发的线程不会同时执行同一块临界区,线程间是互斥执行临界区
  • 同时还表现为可见性,线程在进入临界区时,会从主存中读出这些变量,离开时在把这些变量写入主存。当出现异常时会自动释放锁

用法:

  • 修饰对象方法
  • 修饰类方法
  • 代码块

synchronized是怎么做到线程间互斥执行临界区的呢?是通过监视器来实现,后面会重点讲解监视器

同步方法

在方法的头部包含synchronized关键字。

在实例方法上,锁会和该方法的实例对象关联

public class demo{
     private  int num;
	 public synchronized int getNum(){
	  return num++;
	 }
}

在类方法上,锁会和该方法的类关联

public class demo{
     private static int num;
	 public stataic synchronized int getNum(){
	  return num++;
	 }
}

同步块

常用的单例模式就是用的同步块,锁会和obj对象关联

public class SingletonDemo{
	private SingletonDemo(){}
	private volatile static SingletonDemo singleton;
	private Object obj = new Object();
 	public static SingletonDemo newInstance(){
 		if(singleton == null){
 			synchronized(obj){
 				if(singleton == null){
 					singleton = new SingletonDemo();
 				}
 			}
 		}
 	}
 	return singleton;
}

在jdk1.5之前java程序主要靠synchronized关键字实现锁的功能,jdk1.5之后java新增了一类实现Lock接口的锁,来实现synchronized类似的功能。相比synchronized,锁需要显示地获取和释放,在实际使用中也比synchronized相对灵活

synchronized&lock区别

tips synchronized Lock
实现 JVM控制锁的获取和释放 基于JDK层面实现
使用 不需要手动释放锁 需要手动获取锁和释放锁(finally中unlock)
锁获取超时 不支持,拿不到锁就一直在那等着 支持,可以设置超时时间,时间过了没拿到就放弃
获取锁响应中断 不支持 支持,可以设置是否可以被打断
释放锁的条件 满足一个即可:①占有锁的线程执行完毕 ②占有锁的线程异常退出 ③占有锁的线程进入waiting状态释放锁 调用unlock()方法
公平与否 非公平锁(公平指的是哪个线程等的时间长就把锁交给谁) 默认为非公平锁,可以设置为公平锁(排队等候
public interface Lock {

    //获取锁
    void lock();
   //获取可中断的锁
    void lockInterruptibly() throws InterruptedException;
    //仅当锁在调用时处于空闲状态时才获取锁,true:获取锁,false:未获取到锁
    boolean tryLock();
    //如果在给定的等待时间内是空闲的,则获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    //获取等待通知的组件
    Condition newCondition();
}

ReentrantLock(重入锁)

重入锁可以实现synchronized的同样效果,并且比synchronized灵活

lock&unlock

public class ReentrantLockDemo {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        new Thread(() -> m1(), "A").start();
        new Thread(() -> m1(), "B").start();
    }
    public static void m1() {
        try {
            lock.lock();  //加锁
            System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

tryLock&unLock

尝试锁定,不管是否锁定代码都将继续执行,实际我们可以根据锁定结果做自己的业务逻辑处理

public class ReentrantTryLockDemo {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> m1(), "A").start();
        new Thread(() -> m1(), "B").start();
    }
    public static void m1() {
        //是否获取到了锁
        boolean isLock = lock.tryLock(); 
        try {
            if (isLock) {
            //业务逻辑
                System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
            }
            else {
                System.out.println("线程" + Thread.currentThread().getName() + "未获取到锁");
            }
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            if (isLock) {
                lock.unlock();
            }
        }
    }
}

Fair&Nonfair(公平锁与非公平锁)

公平锁根据线程等待时间长短,来获取锁,等待时间长的可以优先获取锁执行,在性能上非公平锁大于公平锁

我们可以看到通过构造函数来创建公平和非公平锁

  • RentrantLock 有三个内部类 Sync、NonfairSync 和 FairSync 类
    • Sync 继承 AbstractQueuedSynchronizer 抽象类,也就是大名鼎鼎的AQS
    • NonfairSync(非公平锁) 继承 Sync
    • FairSync(公平锁) 继承 Sync
//公平锁
public class ReentrantFairLockDemo {
    //true:公平锁
    private static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        new Thread(() -> m1(), "A").start();
        new Thread(() -> m1(), "B").start();
        new Thread(() -> m1(), "C").start();
    }
    public static void m1() {
        for (int i = 0; i < 6; i++) {
            lock.lock();  //加锁
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}

公平锁
new ReentrantLock(true)时表示公平锁, 线程是交替执行

非公平锁
非公平锁那就随机的获取,谁运气好,cpu时间片轮到哪个线程,哪个线程就能获取锁,和上面公平锁的区别很简单,就在于先new一个ReentrantLock的时候参数为false,当然我们也可以不写,默认就是false

Conditionde

使用Lock和Condition来实现生产者和消费者的同步容器,相比使用wait/notifyAll,使用Conditionde的方式能更加精确地指定哪些线程被唤醒

public class ProducerConsumerLockCondition<T> {
    final private LinkedList<T> list = new LinkedList<>();
    final private int MAX = 20;
    private int count = 0;

    private Lock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();

    public void set(T t) {
        try {
            lock.lock();
            while (list.size() == MAX) {
                producer.await();
            }
            list.add(t);
            ++count;
            consumer.signalAll(); //通知消费者进行消费
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public T get() {
        T t = null;
        try {
            lock.lock();
            while (count == 0) {
                consumer.await();
            }
            t = list.removeFirst();
            count--;
            producer.signalAll();  //通知生产者进行生产
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }
}

总结:

本文主要是简单介绍了编写java多线程程序可能会出现的问题,针对这些问题JDK为我们提供了丰富多样的实现方式。本文介绍的只是其中的冰山一角。后面会逐步介绍内部实现的原理。


转载:https://blog.csdn.net/wangjingjava/article/details/104533893
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场