并发编程的原则:设计并发编程的目的是为了使程序获得更高的执行效率,但绝不能出现数据一致性(数据准确)问题,如果并发程序连最基本的执行结果准确性都无法保证,那并发编程就没有任何意义。
如何控制多线程操作共享数据引起的数据准确性问题呢?
使用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问,也就是**保证我们的共享资源每次只能被一个线程使用,一旦该资源被线程使用,其他线程将不得拥有使用权。**在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
互斥锁:顾名思义,就是互斥访问目的的锁
在java里每个对象都拥有一个锁标记(monitor),也称为监视器。
多线程同时访问某对象时,只有拥有该对象锁的线程才能访问。
在java中可以使用synchronized关键字来标记一个需要同步方法或者同步代码块,当某线程调用该对象的synchronized方法或者访问synchronized代码块,该线程便获得了该对象的锁,其他线程暂时无法访问该对象的锁,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。通过这个方法达到我们上面提到的在同一时刻,只有一个线程能访问临界资源。
**synchronized用法:**分析synchronized同步锁的核心在于他是个对象锁,找清楚锁的对象
synchronized是对象锁,即线程获得的锁是施加在一个实例对象上的,如果不同的线程访问的是同一对象上的不同的同步方法,那么显然不能同时进行。
如果是不同对象上的不同的同步方法,那么就是可以同时进行的。
Synchronized 的作用主要有三个:
从语法上讲,Synchronized 总共有三种用法:
同步代码块
sychronized(synObject){
}
将synchronized作用于一个给定的对象或者类的一个属性,每当有线程执行到这段代码块,该线程会先请求获取对象synObject的锁,如果该锁已被占用,那么新线程只能等待,从而使得其他线程无法同时访问代码块。
/*使用当前对象作为互斥锁
*/
public class SynchronizedTest implements Runnable{
/*在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口
Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。
在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。
*/
private static volatile int m=0;
/*
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
*/
public static void main(String[] args) {
Runnable run=new SynchronizedTest();
Thread thread1=new Thread(run);
Thread thread2=new Thread(run);
thread1.start();
thread2.start();
try {
//join() 使main线程等待这连个线程执行结束后继续执行下面的代码
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m的最终结果:"+m);
}
public void run() {
synchronized (this) {
for(int i=0;i<10000;i++){
m++;
}
}
}
}
特别注意:
Java
中真正能创建新线程的只有Thread
类对象- 通过实现
Runnable
的方式,最终还是通过Thread
类对象来创建线程所以对于 实现了
Runnable
接口的类,称为 线程辅助类;Thread
类才是真正的线程类// 步骤1:创建线程辅助类,实现Runnable接口 class MyThread implements Runnable{ .... @Override // 步骤2:复写run(),定义线程行为 public void run(){ }
1.1:Synchronized是一个重量级锁
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。1.2:Synchronized底层实现原理
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。
synchronized修饰的代码块,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,如果没有释放则需要无限的等待下去。获取锁的线程释放锁只会有两种情况:
1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有。
2、线程执行发生异常,此时JVM会让线程自动释放锁。
Lock与synchronized对比:
1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。
2、synchronized不需要手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
public interface Lock {
//用来获取锁。如果锁已被其他线程获取,则进行等待。
void lock();
// 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
void lockInterruptibly() throws InterruptedException;
//它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
boolean tryLock();
//与tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
Condition newCondition();
}
一般使用Lock时必须在try{}catch{}中进行,并且将释放锁的操作放在finally中,以保证锁一定被释放,防止死锁发送。
Lock接口的常用实现类有ReentrantLock和ReentrantReadWriteLock,它们提供了可重入的互斥锁和读写锁。
使用Lock锁的一般步骤如下:
1. 创建一个`Lock`对象实例。
Lock lock = new ReentrantLock();
2. 在同步的代码块执行完之后,通过调用`unlock()`方法释放锁。
lock.lock();
try{
....
} finally{
lock.unlock();
}