Synchronized
编译器去保证锁的加锁和释放
jdk1.6 引入了偏向锁,轻量级锁(自旋锁)synchronized的优化借鉴了ReenTrantLock中的CAS技术,试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞
编译时加入monitorenter和monitorexit,存放对象头
进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高)
ReenTrantLock
ReentrantLock利用CAS(CompareAndSwap)+AQS。CAS自旋机制保证线程操作的原子性,volatile保证数据可见性和有序性以实现锁的功能。
它的性能比较好也是因为避免了使线程进入内核态的阻塞状态,避免内核态和用户态之间切换.
ReentrantLock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断.
ReentrantLock可以提供公平锁,而synchronized只能是非公平锁.
ReentrantLock可以提高多个线程进行读操作的效率,实现读写锁分离,可以有多个线程同时读,但是当出现写操作时,就只能有一个线程操作。
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
Synchronized和lock区别
1.来源:lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
2.异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。
3.是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
4.是否知道获取锁:Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
synchronized和lock性能区别:
如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
synchronized和lock用途区别:
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
CAS
CAS 是乐观锁的核心算法实现,线程不会阻塞等待,靠底层硬件cpu指令while循环算法,同步时间短情况下性能更好,AtomicInteger等执行时间较短,所以都使用了cas算法
CAS只高效的解决原子操作,可见性和有序性须其他方式保证。
CAS存在三大缺点。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。
AtomicInteger、LongAdder、Lock锁底层,此外,当然还包括 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的
AQS
AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。
AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。一般是同步组件的静态内部类,即通过组合的方式使用。
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)
AQS多线程安全的单例模式
public class DoubleCheckedLocking { // 1
private static volatile Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查,不加volatile,这里会出现问题
instance = new Instance();
} // 8
} // 9
return instance; // 10
} // 11
}
sleep、wait区别
sleep、wait都能使线程进入阻塞状态,但wait会释放Synchronized的对象锁,使其他线程可以重新获取锁,所以wait必须在Synchronized代码块下执行,否则运行时报错。
yield:使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。
ThreadLocal
每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的。
ThreadLocal的应用场景:数据库连接,session管理,可以每个线程一个connect,提高效率,减少多线程资源竞争。
死锁
发生死锁的原因主要由于:
1)线程之间交错执行,解决:以确定的顺序获得锁,所有方法都是先锁A,再锁B。
2)永久等待,解决:使用tryLock()定时锁,超过时限则返回错误信息
死锁:预防(破坏四大条件)与避免(银行家算法)
信号量
linux 系统进程间通信IPC的一种,主要作为进程间以及同一进程不同线程之间的同步手段。
信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv)。
synchronized同时只能有一个线程能访问,而信号量是可以控制同时允许几个线程访问,信号量为1的时候才与synchronized等价。
java在处理线程同步时,常用方法有:
1、synchronized关键字。
2、Lock显示加锁。
3、信号量Semaphore。
Semaphore(信号量)是java.util.concurrent下的一个工具类.用来控制可同时访问特定资源的线程数.内部是通过维护父类(AQS)的 int state值实现.
Semaphore应用场景
Semaphore用来控制访问某资源的线程数,比如数据库连接.假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有20个,这时就必须控制最多只有20个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。
参考文章 https://zhuanlan.zhihu.com/p/27314456
网友评论