批处理——多进程——多线程
在多线程变成中,由于多个线程共享进程的变量,有可能出现同时访问一个资源的情况,因此需要使用同步机制。
Java内存模型规定所有的变量都存在主存当中,每个线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接在主存进行操作。并且每个线程不能访问其他线程的工作内存。
原子操作:原子为不可再分操作。只有对象的读取和赋值是原子操作。int i=10 是原子操作。int x = y;不是原子操作
violation:可见关键字
Synchronized:内部隐示锁
lock: ReentrantLock(显示锁) + ReentrantReadWriteLock(读写锁)
一旦一个共享变量被volatile修饰之后,那么就具备了两层含义。
1)可见性: 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
它会强制将对缓存的修改操作立即写入主存,同时会导致其他CPU对应的缓存无效。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中
2) 有序性: 禁止进行指令重排序。
需要注意的是violation不保证原子性。
比如:
public
volatile
int
inc =
0;
inc++;
在多线程中不同依靠violation关键字保证原子操作。
使用volatile关键字的场景:
1) 多变量的写操作不依赖于当前值
2) 该变量没有包含在具有其他变量的不变式中。
使用例子:状态标记量:
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
把一些不是原子操作组合成原子操作。
(1) synchronized方法
示例代码:
public class InsertData {
public synchronized void insert(){
for(int i=0; i<10;++i){
System.out.println(Thread.currentThread().getName() + ": 执行,变量i = "+i);
}
}
public synchronized void insert2() {
System.out.println(Thread.currentThread().getName()+ "start inset2");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "end inset2");
}
public static synchronized void insert3(){
System.out.println(Thread.currentThread().getName() + "insert3");
}
public void insert4(){
System.out.println(Thread.currentThread().getName()+"insert4");
}
}
测试代码:
public class SynTest {
@Test
public void test() throws InterruptedException {
InsertData insertData = new InsertData();
new Thread(new Runnable() {
@Override
public void run() {
insertData.insert();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
insertData.insert();
}
}).start();
Thread.sleep(8000);
}
}
需要注意的是:
1)当一个线程正在访问一个对象的synchronized方法,那么其他 线程不能访问该对象的synchronized方法,这个原因很简单,是因为对当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2) 当一个线程正在访问对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
3) 如果该方法是非static的,则该锁是对象锁。如果方法是static的,则该锁是类锁。对象锁和类锁不互斥。
4) 对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
(2) synchronized 代码块
synchronized(synObject){
}
当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使其他线程无法同时访问该代码块。
synObject可是是this,代码获取当前对象的锁,也可以是类中的一个属性,也可以是方法的一个object类型的入参,代表获取该对象的锁。
synchronized代码块使用起来比synchronized方法要灵活得多。如果一个方法中只有一部分代码需要同步,如果此时对整个方法用synchronized进行同步,会影响执行效率。而使用synchronized代码块就可以避免这个问题。
synchronized的缺陷:
当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只有两种情况:
1) 获取锁的线程执行完了代码块,然后线程释放对应锁的占有。
2) 线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程变只能干巴巴地等待,严重影响程序执行效率。所以就需要有一种机制可以不让等待的线程一直无限地等待下去(比如只等待一定时间或者能够相应中断),通过lock就可以办到。
1) ReentrantLock:
使用事例:
采用lock,必须主动释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被释放,防止死锁的发生。
Lock lock =new ReentrantLock();
lock.lock();
try{
//执行业务逻辑代码
}catch (Exception e){
}finally {
lock.unlock();
}
2) ReentrantReadWriteLock: 读写锁
readLock用来获取读锁,writeLock用来获取写锁。
读锁可以被多个线程占有。
一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
一个线程已经占用了写锁,则其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
Lock和synchronized的选择:
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
lock是可中断的锁,而synchronized是不可中断锁。lock只能中断等待锁的线程,不能中断正在执行的线程。
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
相同:
1) synchronized和lock都是可重入的锁。
参考资料:http://www.cnblogs.com/dolphin0520/p/3920373.html
http://www.cnblogs.com/dolphin0520/p/3923737.html
http://www.cnblogs.com/dolphin0520/p/3923167.html