多线程同步方法(Java)

12 篇文章 1 订阅

多线程编程同步可能会用到锁,而Java中的锁分为内置锁(Sychronized)和显式锁/重入锁(ReentrantLock)。

一、内置锁(synchronized)

(1)作用于普通方法------锁对象是this
  这里的this指的是当前方法的所属对象,即调用方法的对象。
(2)作用于静态方法------锁对象是当前类的Class对象(class的实例)
  由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。在JVM内部,每个被加载的类都有且只有唯一的一个class的实例与该类加载类对应。
注意:
  给一个方法增加synchronized修饰符之后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的接口方法(接口和抽象类不能被实例化,不能给对象上锁)。
  线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
(3)作用于代码块------锁对象是synchronized(obj)中的这个obj,这个obj一般可用this
  被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。同步块可以更准确地锁定需要同步运行的代码片段,有效地缩小同步范围可以保证并发安全的前提下提高并发效率。

synchronized (this) {//括号里是同步监视器对象,也就是上锁的对象
  //需要同步的逻辑代码
}

注意:
  同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
  同步块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是同步方法所属的主体对象自身。如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,也不是这个类自身,而是这个类对应的java.lang.Class类型的对象。同步方法和同步块之间的相互制约只限于同一个对象之间,所以静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例(对象)没有关系。
  如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块
  synchronized的目的使同一个对象的多个线程,在某个时刻只有其中的一个线程可以访问这个对象的synchronized 数据。每个对象都有一个“锁标志”,当这个对象的一个线程访问这个对象的某个synchronized 数据时,这个对象的所有被synchronized 修饰的数据将被上锁(因为**“锁标志”被当前线程拿走了**),只有当前线程访问完它要访问的synchronized 数据时,当前线程才会释放“锁标志”,这样同一个对象的其它线程才有机会访问synchronized 数据。

二、使用重入锁/显试锁实现线程同步

JDK5中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法:
  ReentrantLock() : 创建一个ReentrantLock实例
  lock() : 获得锁
  unlock() : 释放锁
注:
  ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//需要声明这个锁
private Lock lock = new ReentrantLock();
// 操作方法
public void addMoney(int money) {
  lock.lock();//上锁
  try{
    //操作内容
  }finally{
  lock.unlock();//解锁
}

内置锁与显示锁的区别:
(1)锁的释放
  显示锁必须调用unlock方法才能释放锁,并且需要在finally{}中调用;内置锁只要运行到同步代码块之外就会释放锁。
(2)公平性
  显示锁可以指定公平策略,默认为不公平锁
  内置锁不可以选择公平策略,只能是不公平锁
(3)可中断申请
  显示锁提供可中断申请(lock.lockInterruptibly();可中断申请, 在申请锁的过程中如果当前线程被中断, 将抛出InterruptedException异常)
  内置锁不可中断。(在申请锁时被其它线程持有,那么当前线程后挂起,挂起其间不可中断)
(4)可尝试申请、可定时早请
  显示锁提供尝试型申请方法(Lock.tryLock和Lock.tryLock(long time, TimeUnit unit)),
  内置锁不提供这种特性
(5)是否可以精确唤醒特定线程
  显示锁可以通过Condition对象(由显示锁派生出来),调用Condition.singal或Condition.singalAll方法可以唤醒在该Condition对象上等待的线程。以此来唤醒指定线程。
  内置锁的notify或notifyAll方法唤醒在其上等待的线程,但无法指定特定线程。
  
总结:内置锁够解决大部分需要的场景,只有在需要额外的灵活时,比如公平、可中断、可尝试、可定时、可唤醒特定线程时,我们才考虑用显示锁。如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 。

重入锁的几个方法详解
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c) tryLock (long timeout, TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断。

三、使用特殊域变量(volatile)实现线程同步

volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
  volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不使用它吧。
  原理:每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的,这样就保证了同步

四、使用局部变量实现线程同步

使用ThroadLocal为每一个线程提供一个局部变量,ThroadLocal的原理是Map集合+当前线程
  如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal类的常用方法:
  ThreadLocal() : 创建一个线程本地变量
  get() : 返回此线程局部变量的当前线程副本中的值
  initialValue() : 返回此线程局部变量的当前线程的"初始值"
  set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

ThreadLocal与同步机制 :
(1)ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
(2)前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式

五、wait()、natify()和natifyAll()

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。这三个方法最终调用的都是jvm级的final native方法。随着jvm运行平台的不同可能有些许差异。
(1)如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态
(2)如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行
(3)如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行
具体详见:Thread之七:Object里的wait、notify、notifyAll的使用方法

拓展:

一、使用阻塞队列实现线程同步
  前文5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
  使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
  这里主要是使用LinkedBlockingQueue<>E来实现线程的同步 LinkedBlockingQueue<>E是一个基于已连接节点的,范围任意的blocking queue队列
是先进先出的顺序(FIFO)。

LinkedBlockingQueue 类常用方法:
  LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
  put(E e) : 在队尾添加一个元素,如果队列满则阻塞
  size() : 返回队列中的元素个数
  take() : 移除并返回队头元素,如果队列空则阻塞代码实例: 实现商家生产商品和买卖商品的同步
注:
BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:
add()方法会抛出异常
offer()方法返回false
put()方法会阻塞

二、使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
  原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。
  在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
  
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值

class Bank {
    private AtomicInteger account = new AtomicInteger(100);
    public AtomicInteger getAccount() {
        return account; 
    } 
    public void save(int money) {
        account.addAndGet(money);
    }
}

原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。

参考自:java多线程同步5种方法

三、线程各种方法解析:
yield():切换时间片运行,模拟线程由于cpu时间片用完。
sleep():静态方法,该方法会将执行该方法的线程置于阻塞状态指定的毫秒,当超时后,线程会自动回到Runnable状态下等待在此获取时间片并发执行。
join():协调线程之间的同步运行。thread1中写thread2.join()表示thread2终端等待thread1执行结束后再执行。

Thread.currentThread():获取线程(返回的是运行当前方法的线程)
String getName():获取线程名字
Long getId():获取线程编号
Int priority:获取优先级(1(小)—>10(大) 5—>默认)
注意:线程不能主动获取时间片,所以我们可以通过改善线程的优先级来最大程度的干涉线程调度器分配CPU时间片的几率。理论上,线程优先级越高的线程获取CPU时间片的几率越高。
boolean isAlive:是否活着
boolean isDaemon:是否独立
boolean isInerrupted:是否中断

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值