JAVA多线程与锁机制

JAVA多线程与锁机制

1 关于Synchronized和lock

  • synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码

    • JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。
    • 一个线程访问一个被synchronized修饰的代码块,会自动获取对应的一个锁,并在执行该代码块时,其他线程想访问这个代码块,会一直处于等待状态,自有等该线程释放锁后,其他线程进行资源竞争,竞争获取到锁的线程才能访问该代码块。
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  • Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题。总体上来说,在资源竞争不激烈的情形下,性能稍微比synchronized差点。但是资源竞争非常激烈的时候,synchronized的性能会下降很多,而ReentrantLock的性能表现仍然比较稳定。

2 Java 实现线程安全的三种方式

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

obj 称为同步监视器,也就是锁,原理是:当线程开始执行同步代码块前,必须先获得对同步代码块的锁定。并且任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

  • 同步方法
public synchronized void testThread()
{
    //需要被同步的代码块
}

不需要再指定同步监视器,这个同步方法(非static方法)无需显式地指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。

  • 同步锁,Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块。其中,为了确保能够在必要的时候释放锁,代码中使用finally来确保锁的释放,来防止死锁!

3 Synchronized

  • static synchronized则是该类的所有实例公用一个监视块,静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁

  • 1.方法声明时使用:线程获得的是成员锁

    2.对某一代码块使用:线程获得的是成员锁

    3.synchronized后面括号里是一对象,此时,线程获得的是对象锁

    4.synchronized后面括号里是类,此时,线程获得的是对象锁

4 乐观锁、悲观锁

  • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

    • 可以使用版本号机制(+递归)和CAS算法实现。如果发现数据已经被更改(通过版本号控制),则不更新数据,再次去重复 所需操作直到道没有冲突(使用递归算法)。
  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

4 公平锁、非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死

5 JAVA线程池

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。

  • 提高系统响应速度,降低系统资源消耗

  • 参数:

    • 一个任务被提交到线程池以后,首先会找有没有空闲存活线程(>=corePoolSize的情况)

      • 如果有则直接将任务交给这个空闲线程来执行,
      • 如果没有则会缓存到工作队列中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
    • corePoolSize(核心线程数):

      • 核心线程会一直存活,即使没有任务需要执行
      • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
    • maxPoolSize(最大线程数):

      • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
    • keepAliveTime(线程存活保持时间):

      • 当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
    • workQueue (工作队列):

      新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

    • handler(线程饱和策略):

      • 当线程池和队列都满了,再加入线程会执行此策略。

6 多线程中的i++线程安全吗

不安全。i++不是原子性操作。i++分为读取i值,对i值加一,再赋值给i++,执行期中任何一步都是有可能被其他线程抢占的。

Tags: