对于现在的操作系统和Java来说,多线程并发执行是一种自然而然的操作,支持了多线程,意味着对资源的充分利用,否则单一线程运转的时候,其他人得旁边歇着,这不太合适。
所以同步,就是线程一起执行?
错的,线程的同步不是这个含义,而是在同一个时刻,只有一个线程能执行。 当这个线程执行的时候,它就很自私,等到它用完了资源,其它需要这个资源运行的线程才能执行。
则,异步就是多个线程完成顺序的不确定性。
引入线程的同步也是为了程序的正确执行。 学过操作系统的同学,都知道一个程序有顺序性、封闭性、可再现性这三个特点。对于多个依赖着相同资源的线程,不加处理的情况下,无法保证它们的执行顺序,对于程序的结果自然也无法预知,也就是无法满足进程的那三个特点。
我们自然需要一个机制去帮助我们控制程序的运行,也就是同步机制。
先说两个概念:
临界资源:同一时刻只允许一个线程访问的资源。
临界区:访问临界资源的那段代码。
synchronized关键字就是在Java里实现同步机制的关键字,使用这个关键字,就相当于给资源上了锁,如果当前有一个线程已经访问了这段代码,其它线程是无法去使用这些资源了的,除非获取这些资源的线程将锁释放掉。
synchronized的使用:
两种方式
1)修饰方法
public synchronized static void fun(){
}
2)修饰代码段
对于每一个Java类的对象来说,都有一个叫监视器锁 monitor 的东西,所以是对一个类的实例上锁
public static void fun(){
synchronized (Object.class){
}
}
这个监视器锁,通过反编译会发现,有两个东西,完成对类实例的加锁和释放,一个叫monitorenter,another 叫monitorexit,看起来就不像什么好东西,哦,不是,看起来就知道它们两个的意思了。
一个加锁,一个释放。
每个对象的 monitor 有一个初始值 0 ,在这个对象获取了它的 monitor 锁的时候,这个值就 + 1,重复进入临界区的时候,这个值也会累加,其它线程想要获取这些资源只能等到这个初值为 0 的时候才能得到,否则就阻塞。
即执行monitorenter,值 +1
执行monitorexit,值 -1,直至为 0,表示这段资源已被释放,别的资源可以来尝试争取这个monitor的锁了。
就我接触过,为你们很好的展示synchronized的妙处就是单例模式
单例模式:所有人只有一个对象
//懒汉式
class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
new Thread("A"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
new Thread("B"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
new Thread("C"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
}
}
这里先新启三个进程去得到这个对象的地址,事不过三嘛!感兴趣的童鞋也可以多启动几个,自闭的童鞋可以看我启动。
线程的一些基础知识跟方法,感兴趣的可以去康康
试了几次,发现返回的对象地址一样,这个是线程安全的?
非也。
主要就是它们三个几乎是同一时间完成的,一个线程得到一个实例后,对于剩下两个进程来说, instance == null
这个条件就不成立了,
所以呢,让他们睡一会儿,再看看结果
得到的对象不一定一样了哦,咦,这不是很棒…个屁呀,这是单例模式呀!
所以说,上面那个懒汉式单例模式适用于单线程,多线程不安全!
(此处应有一道光)synchronized登场了
DCL,double checked lock 双重检查单例模式
class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
new Thread("A"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
new Thread("B"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
new Thread("C"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton.getInstance());
}
}.start();
}
}
好了好了,终于给所有人分发一样的对象了,嘿嘿
既然都说到DCL,
这段代码
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
如果没有下面的 if 语句,恰巧有那么一个时刻,两个线程不分先后进入了第一个 if 语句,它们都会得到一个结论,当前还没有线程创建这个类的实例呢!虽然另一个线程可能要等待 monitor 锁,但最终结果可能还会返回相同的对象,你们仔细想想。
好了,这就是一个可以展示synchronized的应用之处,它帮我们得到了正确的程序结果,也就是只返回一个对象。
当然了,上面的单例模式也可以这样加锁
public synchronized static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
只不过这样比较不提倡而已,一旦加锁,就意味着其它可共享资源但是又不怕被更改的资源信息也被锁住了。
打个例子:
你的麻麻在家里做饭,你这会儿很饿很饿,很想搞点东西吃
同步方法就是,你麻麻把厨房门关了,你吃了个寂寞;
同步代码块好比,厨房门不用关,但是锅里米饭没熟、你没法恰、你麻麻也不给恰、不安全,但是馒头、菜应有尽有,你仍然可以大快朵颐,填饱肚子。
以前没有系统的学习多线程的时候,我总觉得synchronized关键字表达的是重量级锁,而volatile表示的是轻量级锁。
volatile关键字表示的是内存可见性。
也就是在Java中,有两个概念,一个叫工作内存,一个叫主内存。主内存上的东西是你拿到的最终值。有这么一种情况,可能你的值在工作内存上发生了更改,但是没有刷新到主内存上时,就会得到了一个错误的结果。
volatile关键字的原理就是,当值被更新时,它会通知到每个线程,您所占有的值xxx发生了改变,请您重新再拷一份新的值到您的工作内存上。
举个比方:
算了,这块暂时想不到什么通俗易懂的例子,就交给你们自己去发现吧
名字、锁的粒度由小到大:
偏向锁,轻量级锁,重量级锁。
其中,轻量级锁也叫乐观锁,重量级锁也称作悲观锁。
在轻量级锁和重量级锁之间,有着两个叫自旋锁和适应性自旋锁的东西,适应性自旋锁是自旋锁的level 2.
这篇博客,并不打算讲关于锁的发展的详细知识,讲不了。万一说不清楚,还把自己绕进去了。程序猿嘛,不就是一个偷懒的活。
况且,很多时候,只需要知道有这么回事就 ok 了。
所以在这里我说一下这些名词的概念,和一些大概知识。
偏向锁:偏心的锁,在当前进程中,大部分时候只有一个线程来获取锁,或者说几乎所有时刻都只有一个线程。 那你想,既然只有这个线程肯跟这个锁 van,它就偏心起来了。一旦这个线程获取了偏向锁,在一个叫MarkWord的东西里,就会保存当前线程的地址,下一次有线程来获取锁的时候,就跟这个地址进行比较,相同就不用再去获取锁、释放锁了,这样就减少了系统的性能消耗。
所以,一旦多出现一个线程来获取锁的时候,这个时候它就觉得自己牛掰了,膨胀了它,就升级到了轻量级锁。
这个时候,没有获取到锁权限的线程不服,我这么美,你不给我获取你的锁?但获取不到就是获取不到。没办法,这线程开始展示自己,它,跳起了芭蕾舞!你不给我?我就在这里跳跳跳、闹太套,就不信你不给我!自旋锁。
锁内心:“ * * ”。
你在那里努力跳,还不如别人随便搞,一时舔狗一时爽,天天舔狗火葬场。“这样下去肯定不行的、这样,不如把它烤…”,走错片场了不好意思。所以,自旋适应锁可以改变等待获取锁的时间。
当越来越多的线程来争夺锁权限的时候,会更加膨胀起来,重量锁,也是粒度最大的锁。
乐观锁:嘴上笑嘻嘻,心里嘤嘤嘤。它老是 jio 着它,对它所占用的资源来说,别的线程访问时,总是读多写少。
悲观锁:哭唧唧,总觉得别的线程要更改它所拥有的资源。
所以不要一提到synchronized关键字,就觉得它很沉重,其实到现在为止,Java已经对它做了很多的优化了,正如它的膨胀过程一样。
那,对于锁膨胀的详细介绍,我给大家分享一些网址,当然了,这些都很容易在网上搜到。人家写的很全面、很细致、很详细,很好非常好明天会更好。我把它们放在我的博客里,因为你点进来这篇博客就是有缘嘛,有缘当然要推荐给你咯。若想对锁的认识进一步加深的童鞋可以看看,不感兴趣的可以看哦。-.-
锁的介绍1
锁的介绍2
锁的介绍3