并发编程的演进:

批处理——多进程——多线程


在多线程变成中,由于多个线程共享进程的变量,有可能出现同时访问一个资源的情况,因此需要使用同步机制。


java的内存模型:

Java内存模型规定所有的变量都存在主存当中,每个线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接在主存进行操作。并且每个线程不能访问其他线程的工作内存。



关键字:

原子操作:原子为不可再分操作。只有对象的读取和赋值是原子操作。int i=10 是原子操作。int x = y;不是原子操作

violation:可见关键字

Synchronized:内部隐示锁

lock: ReentrantLock(显示锁) + ReentrantReadWriteLock(读写锁)



violation关键字:

一旦一个共享变量被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);




synchronized:

把一些不是原子操作组合成原子操作。

(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就可以办到。


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都是可重入的锁。