这是为解决“非线程安全”问题,当多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题;
概念:所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来
为甚麽需要同步处理呢?
假如我们设计一个卖票的程序(run方法中模拟延时用sleep),用三个多线程同时来卖,当我们执行后,剩余票数会出现负的情况,这是因为这几个线程都毫无规律的同时抢占资源来卖票,在最后一轮不免有多个线程进入卖票,导致为负;
同部处理需要关键字synchronized来实现 ;
根据synchronized关键字修饰不同代码,可划分为如下:
分类 | 具体分类 | 被锁的对象 | 伪代码 |
---|---|---|---|
方法 | 实例方法 | 类的实例对象 | public synchronized void fun(){} |
方法 | 静态方法 | 类对象 | public static synchronized void fun(){} |
代码块 | 实例对象 | 类的实例对象 | synchronized (this){} |
代码块 | class对象 | 类对象 | synochronized(SynochronizedDemo.class){} |
代码块 | 任意实例对象Object | 实例对象Object | String s = “ss”; synochronized(s){} |
这种方式是在方法里拦截的,也就是说进入到方法中的线程依然可能会有多个。
下面看一个卖票的例子:
class MyThreadTick extends Thread {
@Override
public void run() {
int tick = 10;
while(tick > 0) {
System.out.println("剩余票数:"+(tick--));
}
}
}
class MyRunnableTick implements Runnable {
private int tick = 20;
@Override
public void run() {
//延时代码必须在这,如果放在synchronized代码块内,则第一个线程进入后,其他线程就没有机会进入
//了;
while(tick>0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
if(tick > 0) {
System.out.println(Thread.currentThread().getName()+"进行了销售,剩余票数:"+ --tick);
if(tick == 0) {
System.out.println("票已经卖完了!!!!");
}
}
}
}
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
Runnable myRunnableTick = new MyRunnableTick();
new Thread(myRunnableTick,"销售A").start();
new Thread(myRunnableTick,"销售B").start();
new Thread(myRunnableTick,"销售C").start();
}
}
输出:
销售B进行了销售,剩余票数:19
销售A进行了销售,剩余票数:18
销售C进行了销售,剩余票数:17
销售A进行了销售,剩余票数:16
销售B进行了销售,剩余票数:15
销售C进行了销售,剩余票数:14
销售A进行了销售,剩余票数:13
销售B进行了销售,剩余票数:12
销售C进行了销售,剩余票数:11
销售B进行了销售,剩余票数:10
销售A进行了销售,剩余票数:9
销售C进行了销售,剩余票数:8
销售B进行了销售,剩余票数:7
销售A进行了销售,剩余票数:6
销售C进行了销售,剩余票数:5
销售B进行了销售,剩余票数:4
销售C进行了销售,剩余票数:3
销售A进行了销售,剩余票数:2
销售B进行了销售,剩余票数:1
销售C进行了销售,剩余票数:0
票已经卖完了!!!!
class MyRunnableTick implements Runnable {
private int tick = 20;
@Override
public void run() {
while(tick>0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
public synchronized void sale() {
if(tick > 0) {
System.out.println(Thread.currentThread().getName()+"进行了销售,剩余票数:"+(--tick));
if(tick == 0) {
System.out.println("票已经销售完了!!!!");
}
}
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
Runnable myRunnableTick = new MyRunnableTick();
new Thread(myRunnableTick,"销售A").start();
new Thread(myRunnableTick,"销售B").start();
new Thread(myRunnableTick,"销售C").start();
}
}
销售A进行了销售,剩余票数:19
销售B进行了销售,剩余票数:18
销售C进行了销售,剩余票数:17
销售C进行了销售,剩余票数:16
销售B进行了销售,剩余票数:15
销售A进行了销售,剩余票数:14
销售C进行了销售,剩余票数:13
销售B进行了销售,剩余票数:12
销售A进行了销售,剩余票数:11
销售C进行了销售,剩余票数:10
销售A进行了销售,剩余票数:9
销售B进行了销售,剩余票数:8
销售C进行了销售,剩余票数:7
销售A进行了销售,剩余票数:6
销售B进行了销售,剩余票数:5
销售B进行了销售,剩余票数:4
销售A进行了销售,剩余票数:3
销售C进行了销售,剩余票数:2
销售C进行了销售,剩余票数:1
销售A进行了销售,剩余票数:0
票已经销售完了!!!!
同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度会很慢。
在jdk1.6之前,synchronized是一个重量级锁;我们都知道,在Object中维护了一个监视器Monitor,synchronized正是通过这个监视器来实现同步的,监视器锁的本质是依赖于底层的操作系统mutex lock(互斥锁)来实现的,每个对象都对应一个可称为互斥锁的标记,这个标记用来保证在任意时刻只能有一个线程访问该锁;
老版本jdk的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。
class Sync {
public synchronized void test() {
System.out.println(Thread.currentThread().getName()+": test方法开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": test方法结束");
}
}
class Mythread extends Thread {
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
for(int i = 0; i<3; i++) {
Mythread mythread = new Mythread();
mythread.start();
}
}
}
输出:
Thread-2: test方法开始执行
Thread-0: test方法开始执行
Thread-1: test方法开始执行
Thread-1: test方法结束
Thread-0: test方法结束
Thread-2: test方法结束
前三行几乎同时输出,后三行在一秒后同时输出,这说明了这三个线程同时进入了被synchronized修饰的方法内,可见并没有达到锁住多个线程进入这个方法的目的,这是为甚麽呢?
原因:
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
要想达到我们的目的,我们可以将上面代码改变一下,让锁住同一个对象 :
class Sync {
public void test() {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+": test方法开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": test方法结束");
}
}
}
class Mythread extends Thread {
private Sync sync;
public Mythread(Sync sync) {
this.sync = sync;
}
@Override
public void run() {
sync.test();
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
Sync sync = new Sync();
for(int i = 0; i<3; i++) {
Mythread mythread = new Mythread(sync);
mythread.start();
}
}
}
输出:
Thread-0: test方法开始执行
Thread-0: test方法结束
Thread-2: test方法开始执行
Thread-2: test方法结束
Thread-1: test方法开始执行
Thread-1: test方法结束
为甚麽锁住这个类的Class对象就行了呢,我们来回顾一下反射:
在JVM中任何一个类都有一个唯一的Class对象 ,此对象记录该类的组成结构,通过该class对象,可以反向查找到这个类的信息,称之为反射;
class Sync {
public void test() {
synchronized (this.getClass()){ //通过对象获取Class对象,getClass()方法是Object类中的;
System.out.println(Thread.currentThread().getName()+": test方法开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": test方法结束");
}
}
}
class Mythread extends Thread {
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
for(int i = 0; i<3; i++) {
Mythread mythread = new Mythread();
mythread.start();
}
}
}
使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。 因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
#####2.CAS的操作过程
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。
老版jdk的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。
我们可以用等红绿灯作为例子。Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如我们在同步代码块中只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更合适。
然而,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞。JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)。
就我们的例子来说,如果之前不熄火等待了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等待绿灯,那么这次不熄火的时间就短一点。
公平性
自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
内建锁无法实现公平机制,而lock体系可以实现公平锁
多线程同一时间访问同一个资源(锁),产生竞争,线程阻塞和唤醒,这将会带来很大的效率问题!
JVM实现,开发者经过大量的程序分析,多线程访问同一资源的时候,大多数情况下并不是同一时间进行的。
所以有了优化:
无锁: 无同步不使用synchronized;
偏向锁:一个线程访问一个资源,产生竞争(多线程)
轻量级锁:多线程不同时间访问同一资源
重量锁:多线程同一时间访问同一资源
这三种都使用了synchronized关键字,但具体是哪一种锁,我们也不知道,这是由JVM决定的,这是JVM对synchronized的优化;
锁的升级(膨胀):无锁 —> 偏向锁 ----> 轻量级锁 ----> 重量级锁
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。
其他优化: (开发者的优化)
锁的粗化:将连续的加锁解锁变成更大范围的加锁解锁(人为的优化,写代码的优化层次)
锁消除:即删除不必要的锁;
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
StringBuff属于线程安全,每次执行appand方法就会加锁,完毕时会解锁,这样会让执行效率大大降低,而且,根本就不会有多线程来同时访问StringBuff对象,所以没有必要加锁,所以使用StringBuilder,其实一般都是使用StringBuilder;
死锁可以用下面这张图来表达:
设计一个死锁:
class Pen {
private String pen = "pen";
public String getPen() {
return pen;
}
}
class Book {
private String book = "book";
public String getBook() {
return book;
}
}
public class TestLockClear {
private static Pen pen = new Pen();
private static Book book = new Book();
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
synchronized(pen) {
System.out.println("i have pen ,but not have book");
}
synchronized (book) { //这个synchronized一定得包含在上面那个之内才能达到死锁的效果
System.out.println("i hava pen and book");
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(book) {
System.out.println("i have book ,but not have pen");
}
synchronized (pen) {
System.out.println("i hava pen and book");
}
}
});
thread.start();
thread1.start();
}
}
有一定概率出现死锁:(如下结果,一直停止不了)
book: i have book ,but not have pen
pen: i have pen ,but not have book
在set方法中,每个线程都会新建自己的map,所以不同线程拥有自己的独立的map;
ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说ThreadLocal 可以为每个线程创建一个单独的变量副本,相当于线程的 private static 类型变量。
ThreadLocal 的作用和同步机制有些相反:同步机制是为了保证多线程环境下数据的一致性;而 ThreadLocal 是保证了多线程环境下数据的独立性。
下面看它的简单应用:
public class TestThreadLocal {
private static ThreadLocal<String> threadLocal = new ThreadLocal();
private String commonString = "thread---A";
public static void main(String[] args) {
threadLocal.set("alla");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("thread----子线程");
System.out.println(threadLocal.get()); //thread----子线程
}
},"子线程");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}
}
结果:
thread----子线程
alla
从运行结果可以看出,对于 ThreadLocal 类型的变量,在一个线程中设置值,不影响其在其它线程中的值。也就是说ThreadLocal 类型的变量的值在每个线程中是独立的。
先看看set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
关于引用有下面这些类型:
先做了解:
强引用:
example:String str = “hello”;
软引用
弱引用:
幻引用(幽灵引用)