一.Synchronized
使用:
1.修饰方法:同步方法
2.修饰局部代码块:同步代码块
-
2.1 同步代码块时,锁本类对象
-
2.2 同步代码块时,锁本类的字节码
-
2.3 同步代码块时,锁本类的成员属性对象.
-
2.4 同步代码块时,锁本类的类属性对象.
package cn.tedu.thread;
/**
* 测试synchronized的加锁效果.
* 1.同步方法
* 2.同步代码块
* 2.1 同步代码块时,锁本类对象
* 2.2 同步代码块时,锁本类的字节码
* 2.3 同步代码块时,锁本类的成员属性对象.
* 2.4 同步代码块时,锁本类的类属性对象.
*/
public class SynchonizedDemo1 {
public static void main(String[] args) {
MyThread mt = new MyThread();
for (int i = 0; i < 5; i++) {
new Thread() {
public void run() {
mt.sayHello4();
};
}.start();
}
}
}
class MyThread {
/**
* synchronized修饰方法,同步方法,这个和synchronized (this)作用相同
* 1.如果多线程调用同一个对象实例的sayhello4方法,也就是锁住了本方法。
* 则synchronized锁住方法,必须一个线程执行完毕下个线程才能获取到锁
* 2.如果多线程调用的不是同一个对象的sayHello4方法,也就是执行的不是同一个本类实例对象的sayHello4方法。
* 则synchronized锁住的方法无效。
*/
public synchronized void sayHello4() {
try {
Thread.sleep(1000);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": hello" + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* synchronized锁住本类的类属性,也就是synchronized加锁的对象是另外一个对象了,而这个对象事本类的类属性,也就是本对象只有一份。
* 不管是不是调用同一个对象实例的sayHello3方法,因为synchronized加锁的是i2对象,如果i2对象相同
* synchronized锁住的代码块都生效,因为这个锁匙是对字节码生效的。
*/
private static Integer i2 = new Integer(1);
public void sayHello3() {
synchronized (i2) {
try {
Thread.sleep(1000);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": hello" + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* synchronized锁住本类的对象属性,也就是synchronized加锁的对象是另外一个对象了。
* 1.如果多线程调用同一个对象实例的sayhello2方法, 则synchronized锁住的代码块类,必须一个线程执行完毕下个线程才能获取到锁
* 2.如果多线程调用的不是同一个对象的sayHello2方法。 则synchronized锁住的代码无效。
*/
private Integer i1 = new Integer(1);
public void sayHello2() {
synchronized (i1) {
try {
Thread.sleep(1000);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": hello" + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* synchronized锁住类,也就是锁住字节码 多线程调用同一个对象实例的sayhello1方法,或者不通对象实例的sayHello1方法
* synchronized锁住的代码块都生效,因为这个锁匙是对字节码生效的。
*/
public void sayHello1() {
synchronized (MyThread.class) {
try {
Thread.sleep(1000);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": hello" + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* synchronized锁住对象,
* 1.如果多线程调用同一个对象实例的sayhello方法,也就是锁住了本方法所属的对象实例。
* 则synchronized锁住的代码块类,必须一个线程执行完毕下个线程才能获取到锁
* 2.如果多线程调用的不是同一个对象的sayHello方法,也就是执行的不是同一个本类实例对象的sayHello方法。
* 则synchronized锁住的代码无效。
*/
public void sayHello() {
synchronized (this) {
try {
Thread.sleep(1000);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": hello" + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
我们用javap反编译.class文件,得出如下.
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: returnpublic void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
synchronized的原理是:
1.修饰方法的时候,会在方法的开始加上标识:ACC_SYNCHRONIZED
2.修饰代码块时,会在代码起始monitorenter和结束的地方monitorexit.
无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。
ObjectMonitor类中提供了几个方法,如enter、exit、wait、notify、notifyAll等。sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。(关于Monitor详见深入理解多线程(四)—— Moniter的实现原理)
二. volatile
1.volatile关键字用于指定变量的可见性.这个要根据JVM模型来说,每个线程在运行时都会去拷贝主内存的中的数据进入自己的线程私有区域,也就是工作区域.
而volatile关键字修饰的变量,每次使用时都要去主内存重新取值,并且当这个变量发生改变的时候,改变的值要立马同步到主内存中去.
2.禁止指令重排序,单例模式中的懒加载双重判断
//volatile修饰属性,表示属性从主内存中取,保证age的多线程可见性
private volatile int age;
三.ThreadLocal
原理,用TreadLocal封装的对象,会首先获取当前线程的map容器.在池中寻找我们包装的对象,如果对象存在则取用,如果不存在则创建,有点spring容器的思想.这个保证了每一个线程有一个对象的实例.
具体代码步骤:
1.创建ThreadLocal对象
2.重写覆盖initialValue方法.返回我们包装的工具类
3.每个线程内部调用ThreadLocal对象的get()方法,获取线程容器中的simpleDateFormat对象.
public class ThreadLocalDemo {
//1.创建ThreadLocal对象
public static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() {
//2.重写覆盖initialValue方法.返回我们包装的工具类
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:ss");
};
};
public static void main(String[] args) {
for(int i = 0;i < 10 ;i ++) {
new Thread() {
public void run() {
System.out.println(Thread.currentThread().getName());
//3.每个线程内部调用ThreadLocal对象的get()方法,获取线程容器中的simpleDateFormat对象.
System.out.println(tl.get().format(new Date()));
};
}.start();
}
}
}
底层实现原理解释:
1.get()方法首先会获取执行当前代码的线程.
2.获取线程的ThreadLocalMap容器.
3.判断容器是否存在并且容器中是否存在我们需要的对象.如果存在则取值返回.
4.如果容器不存在或者容器中不存在我们需要的数据,则执行setInitialValue()方法.
5.setInitialValue()方法调用我们重写的initialValue()方法,获取我们需要的对象实例并返回.
6.将此对象存入线程容器ThreadLocalMap中去,保证了线程单例.
setInitialValue()方法的调用.
threadLocal存在内存泄露的问题:
由于thread.threadLocalMap中的key为threadLocal本身,且threadLocal本身是用的弱应用,则key在gc触发时则被回收,则key变为null,而value依然存在.则就可能出现内存泄露,但是只要随着线程的消亡,泄露的内存自然也就回来了.
四.多线程并发问题的解决
多线程并发问题的解决思路:
1.不共享资源,则不会发生并发问题
2.运用synchronized关键字,同步锁住,当一个线程在操作该变量时,则其他线程不能操作.
3.用ReentrantReadWriteLock读写锁,锁住写的操作,读的操作是共享的
4.使用ThreadLocal封装一个想要共享的变量,把他封装之后,则相当于他给每个线程复制了一份该共享的变量,但是这样的锁不能解决ticket,买票的问题,因为买票的共享的变量必须相互之间有联系.
具体代码:
public class DateUtil{
//1.直接写试用多线程调用SimpleDateFormat对象会出现并发现象
//定义一个成员变量,日期格式化器对象
private static final SimpleDateFormat df =
new SimpleDateFormat("yyyy-MM-dd hh:ss");
//定义一个方法,用SimpleDateFormat的对象将字符串转换成Date类型
public static Date parse(String s) throws Exception {
return df.parse(s);
}
//解决方案1,将调用df对象的方法,加上同步锁
synchronized public static Date parse2(String s) throws Exception {
return df.parse(s);
}
//解决方案2,将df对象定义到方法里,每个方法都有一个SimpleDateFormat对象,
//不共享对象就不会产生多线程并发
public static Date parse3(String s) throws Exception {
SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd hh:ss");
return df2.parse(s);
}
//解决方案3,将SimpleDateFormat对象用ThreadLocal包装一下
private static final ThreadLocal<DateFormat> tl= new ThreadLocal<DateFormat>() {
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:ss");
};
};
//定义parse4方法
public static Date parse4(String s) throws Exception {
return tl.get().parse(s);
}
}
五.线程的锁
锁的分类:
可以分为悲观锁和乐观锁(是否全部加锁);
可以分为公平锁和非公平锁(是否需要排队);
线程锁实例:
public class TicketRunnable implements Runnable{
//定义总共30张票,这个票声明为对象的成员变量,则每个线程都能共享该变量
int ticket = 30;
//声明一个读写锁对象,这个读写锁是为了防止多线程并发是修改ticket出现的问题.
public static final ReentrantReadWriteLock rrwLock =
new ReentrantReadWriteLock();
@Override
public void run() {
while(true) {
try {
//给ticket加上也个write()的锁
rrwLock.writeLock().lock();
//如果票卖完了,则输出票已卖完,并且退出循环
if(ticket <= 0) {
System.out.println(Thread.currentThread().getName()
+ " 票已卖完");
break;
}
//让线程睡眠50毫秒,以免单个线程执行速度过快,一次性就把票卖完了,看不出效果.
Thread.sleep(50);
//输出当前执行的线程的名字和余票数,并且修改余票是多少.
System.out.println(Thread.currentThread().getName()
+" 当前出票票号"+" 20190815" + (int)(Math.random()*100)
+ " 余票:" + --ticket + "张");
} catch (Exception e) {
// TODO: handle exception
}finally {
//最后关闭读写锁,以免占用更多的内存空间
rrwLock.writeLock().unlock();
}
}
}
六.多线程安全的API
1.hashTable这是通过Synchronized关键字实现的
2.Vector这也是加Synchornized实现的
3.StringBuffer这也是加Synchornized实现的
4.CopyOnWriteArrayList系列容器效率高于vector,并且线程安全的底层实现写时复制,有点是效率高于synchronized,但是会有变量副本存在,且数据强一致性不能保证,只能保证最终一致性.
5.ConcurrentHashMap jdk1.7采用的是segment分段式锁,即锁细化;jdk1.8之后采用的CAS+自旋加锁
七.synchronized和lock的区别
1.synchronized是java中的关键字,lock是接口是实现类.
2.synchronized锁机制是JVM实现的,lock是java类实现的.
3.synchronized是悲观锁,自由度不高,lock是乐观锁,可以高度自由的定制锁.
4.synchronized是用的JVM指令完成的额锁,lock是用的CAS(compare and swap)算法(比较并替换).
5.synchronized只能是非公平锁,而lock可以实现公平和非公平锁(实现公平锁的机制是lock有一个队列记录排队顺序.)
6.synchronized是重量锁,lock锁的CAS算法在高并发的情况下,可能会造成并发等待,造成多线程等待.
说明:
CAS(compare and swap)算法:比较并替换算法原理,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。但是高并发情况下大量的CAS算法,可能会造成CPU负荷非常大,得不到锁的线程会持续的比较比较…
//定义一个原子类
private AtomicInteger atomicInteger = new AtomicInteger(0);
//利用CAS原理加锁
private final int getAndIncrement(){
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
}while (!this.atomicInteger.compareAndSet(current,next)); //第一个参数是期望值,第二个参数是修改值是
System.out.println("*******第几次访问,次数next: "+next);
return next;
}
具体参考文章:https://blog.csdn.net/qq_29373285/article/details/85164190
八.提高锁性能:
一.lock层面
1,减小锁持有时间:锁持有时间越长,相对的锁竞争程度也就也激烈。方法就是在我们编码时,只在必要的代码段进行同步,没有线程安全的代码,则不要进行锁。这样有助于降低锁冲突的可能性,进而提升系统的并发能力。
2,减小锁粒度:字面意思就是如何锁更小的资源。最典型的例子就是HashTable和ConcurrentHashMap,前者是锁住了整个对象,而后者只是锁住其中的要处理的Segment段(因为真正并发处理的这个Segment)。好比,人吃饭的,还可以看电视(锁住的是嘴,吃饭的时候不能喝茶,而不是整个人)。这里看下这个 HashMap、HashTable和ConcurrentHashMap的文章(http://www.cnblogs.com/-new/p/7496323.html)不了解的可以看下,更深刻理解JDK是如何高效的利用锁的。
3,读写锁替换独占锁:在读多写少的情况下,使用ReadWriteLock可以大大提高程序执行效率。相对容易理解不再赘述。
4,锁分离:读写锁分离了读操作和写操作,我们在进一步想,将更多的操作进行锁资源分离,就锁分离。典型的例子:LinkedBlockingQueue的实现,里边的take()和put(),虽然都操作整个队列进行修改数据,但是分别操作的队首和队尾,所以理论上是不冲突的。当然JDK是通过两个锁进行处理的.
5,锁粗化:前边我们说了:减少锁持有时间、减小锁粒度。这里怎么又有锁粗化了。前者是从锁占有时间和范围的角度去考虑,这里我们从锁的请求、同步、释放的频率进行考虑,如果频率过高也会消耗系统的宝贵资源。典型场景:对于需要锁的资源在循环当中,我们可以直接将锁粗化到循环外层,而不是在内层(避免每次循环都申请锁、释放锁的操作)。
二,看下JVM对锁的优化:
1,偏向锁:如果第一个线程获得了锁,则进行入偏向模式,如果接下来没有其他线程获取,则持有偏向锁的线程将不需要再进行同步。节省了申请所、释放锁,大大提高执行效率。如果锁竞争激烈的话,偏向锁将失效,还不如不用。可以通过-XX:+UseBiasedLocking进行控制开关。
2,轻量级锁:将资源对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁,如果线程获得(CAS)轻量级锁成功,则可以顺利进入同步块继续执行,否则轻量级锁失败,膨胀为重量级锁。 其提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(前人经验数据)。
3,自旋锁:偏向锁和轻量级锁都失败了,JVM还不会立刻挂起此线程,JVM认为很快就可以得到锁,于是会做若干个空循环(自旋)来重新获取锁,如果获取锁成功,则进入同步块执行,否则,才会将其挂起。
4,锁消除:JVM在编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。我们在利用JDK一些类的时候,自己没有加锁,但是内部实现使用锁,而程序又不会出现并发安全问题,这时JVM就会帮我们进行锁消除,提高性能。例如: