1 线程与进程
一个程序至少需要一个进程,而一个进程最少需要一个线程。
2 Thread的方法
1)start(),调用该方法执行该线程。
2)stop(),结束线程
3)join(),等上个线程执行完毕,再加入
4)sleep(),使线程进入阻塞,但不会释放锁。
5)yield(),释放线程,但不会释放锁。
6)run(),调用该方法直接执行线程run()方法,但是线程调用start()也是执行run().区别:一个是由线程执行run(),一个是直接调用线程的run().
3 Object中的多线程方法
1)wait()线程挂起,对象锁会被释放。直到时间达到(设置参数)或者被notify()或者notifyALl()
2)notify()
3)notifyAll()
4 线程的状态
- new 状态,并没有被start()调用之前
- 就绪状态,调用start()进入就绪状态,但并不是说只有调用start()方法,就马上变成当前线程,在变为当前线程之前都是就绪状态。线程在睡眠和挂起中恢复的时候也是就绪状态。
- 运行状态,线程被设置成当前线程,并执行run()方法。
- 阻塞状态,线程被暂停等。
- 死亡状态,线程执行结束。
5 锁类型
- 可重入锁,在执行对象中所有同步方法不用再次获得锁
- 可中断锁,在等待获取锁的过程中可中断
- 公平锁,按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁的权利
- 读写锁,对资源的读取和写入的时候分为2部分处理,读的时候可以多线程一起读,写的时候必须同步的写
Synchronized和Lock的区别
类别 | synchronized | ReentrantLock |
存在层次 | Java关键字,JVM层次上 | 类 |
锁的释放 | 1已获取锁的线程执行完毕后,释放锁 2线程执行发生异常,JVM会让线程释放锁 | 在finally中释放锁,要不然会发生线程死锁 |
锁的获取 | 假设A线程获取了锁,B线程等待。 如果A线程阻塞,B线程会一直等待 | tryLock |
锁的状态 | 无法判断 | 可以判断 |
锁的类型 | 可重入,不可中断,非公平 | 可重入,可中断,公平 |
性能 | 少量同步 | 大量同步 |
synchronized
synchronized使用场景
- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获取当前对象实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获取当前对象实例的锁
- 修饰代码块,指定加锁对象,对指定对象加锁,进入同步代码前要获取当前对象实例的锁。但是synchronized(class)是给class类上锁
synchronized的使用
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance采用的volatile关键字也是必要的,此段代码会分3步执行:
1 为uniqueInstance分配内存空间
2 初始化uniqueInstance
3将uniqueInstance指向分配的内存地址
因为JVM具有指令重排的特性,执行顺序可能会是1->3->2。指令重排在单线程下不好有问题,但是在多线程下会导致一个线程获取了还没有初始化的实力。例如线程T1执行了1和3,此时T2调用getUniqueInstance()发现uniqueInstance不为空,因此返回了uniqueInstance,但此时uniqueInstance并未被初始化。
使用volatile可以禁止JVM指令重排,保证在多线程环境下也能正常运行。
synchronized底层原理
synchronized关键字底层原理属于JVM层。
①synchronized同步语句块:使用的是monitorenter和monitorexit。其中monitorenter指令指向同步代码块的开始位置,锁计数+1,monitorexit指明同步代码块的结束位置,锁计数-1.当计数器为0时,释放锁。编译的时候会有2个monitorexit,一个是执行完释放,一个是发生异常JVM释放。
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过javap -c -s -v -l SynchronizedDemo.class可以看到JVM的执行过程
②synchronized修饰方法:当synchronized修饰方法,并没有monitorenter和monitorexit指令。取而代之的是ACC_SYNCHRONIZED标识。这个标识指明该方法是一个同步方法,JVM通过ACC_SYNCHRONIZED访问标识来辨别一个方法是否为同步方法,从而执行同步调用。、
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
Java早期的版本,synchronized属于重量级锁,因为监视器monitor依赖于底层操作系统mutex lock来实现。java的线程要映射到操作系统的原生线程上。如果挂起或者唤醒一个线程,都需要操作系统来完成,而操作系统实现线程之间的切换时需要从用户状态转为内核状态,这个状态之间的转换需要大量时间。
synchronized 1.6之后的优化
1.6引用了偏向锁,轻量级锁,自旋锁,适应新自旋锁,锁消除,锁粗化等
锁主要有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁。随着竞争激烈而逐渐上升
偏向锁,偏向锁在无竞争的时候会把整个同步都消除掉。意思是偏向于第一个获取他的线程,如果接下来的操作中,该锁没有被其他线程锁获取,那么就取消锁。《深入理解Java虚拟机:JVM高级特性与最佳实践》第13章,第3节锁优化。
轻量级锁,在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消除,因为在使用轻量级锁时,不需要申请互斥量,另外,轻量级锁的加锁解锁都用到了CAS操作。依据“对于绝大部分锁,在整个同步周期内部是不存在竞争的”如果没有竞争,使用CAS操作避免了互斥操作的开销。
自旋锁和自适应自旋,一般线程持有锁的时间都不是很长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的,为了让一个线程等待并不是讲他挂起,而是让这个线程执行一个忙循环(自旋,一般设置为10次),这项技术叫做自旋这样避免了线程切换的开销。1.6引入自适应自旋锁:自旋的时间不在固定,而是和前一次同一个锁上的自旋时间以及锁的拥有着的状态锁决定的。
锁消除,把不必要的同步在编译阶段消除。并不是将我们自己编写的代码的同步消除,例子
String str1="qwe";
String str2="asd";
String str3=str1+str2;
1.5之前底层的实现
StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd");
StringBuffer 是一个线程安全的类,那么这两个append方法都会同步,我们发现这段代码不存在线程安全问题,这个时候会吧这个同步锁消除。
锁粗化,以上面的为例,每一个append要一个同步,那么可以把锁粗化第一个append和最后一个。
synchronized缺点:
当有多个线程读写文件时,读操作和写操作发生冲突,写操作与写操作发生冲突,但是读操作和读操作不发生冲突现象。
但是如果使用了synchronized,会导致一个线程进行读操作时,其他线程无法进行读操作。
ReentrantLock
Lock
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()获取锁,如果锁已被其他线程获取,则进行等待。如果使用lock()必须主动释放锁,所以要加入finally。
tryLock()尝试获取锁,如果成功返回true失败返回false。
lockInterruptibly()通过这个方法获取锁时,如果线程正在等待锁,则这个线程能够响应中断,也就是说当2个线程通过lock.lockinterruptibly()获取锁时,若此时线程1获取了锁,而线程2只有在等待,那么对线程2调用Thread.interrupt()方法能中断线程2的等待。
注意,当一个线程获取了锁以后,是不会被interrupt()方法中断的。
ReentrantLock
可重入锁。实现了Lock接口
ReadWriteLock
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock writeLock();
}
一个用来读锁,一个用来写锁,分成2个锁来分配给线程,从而使多个线程能进行读操作。但是如果有一个线程占用了读锁,此时有其他线程如果要申请写锁,要等读锁被释放。