1,造成线程安全问题的原因
①存在线程间的共享数据(例如堆内存)
②存在多个线程同时操作共享数据
2,锁类型
互斥锁:(重量级锁)同一时刻有且仅有一个线程操作共享数据,当某个线程正在操作共享数据时,其他线程处于等待状态,必须等到该线程处理完后再进行.
偏向锁:java1.6的新锁,针对多个线程竞争较少的场景,如果一个线程获得了锁,那么锁就进入偏向模式,对象头Mark Word里面的锁结构也编程偏向锁结构,这个线程再次请求锁时,无需做任何操作。
轻量级锁:依据是大部分的锁,整个同步周期内不存在竞争,适用线程交替执行同步块的场景。
自旋锁:依据是大部分线程持有锁的时间不会太长,因此自旋锁假设很快当前线程就会获得锁,所以虚拟机会让想要获取锁的线程执行几个空循环,如果循环完成后得到锁,则顺利进入。否则就会将线程在操作系统层面挂起。
锁消除:指虚拟机在编译时消除无用的锁。
3,synchronized关键字作用:
①保证同一时刻只有一个线程可以执行某个方法或者代码块
②可以保证一个线程操作的共享数据变化能立即被其他线程所见(可替代volatile)
4,synchronized关键字用法:
①修饰实例方法:作用于当前实例的方法(不包括静态方法),进入同步代码前要获取当前实例的锁(JVM通过常量池中的方法结构表的acc_synchronized标识区分一个方法是否是同步方法。)
public class SynchronizedClass extends Thread
{
static int share = 0;
//①修饰实例方法
public synchronized void increase()
{
share++;
}
//②修饰静态方法
public synchronized static void increase4ClassLock(){
share++;
}
public void run()
{
for (int i = 0; i < 1000; i++)
{
increase();
}
}
/**
* @param args
*/
public static void main(String[] args)
{
try
{
//场景1,t1,t2线程执行需要获得实例sc1的互斥锁
SynchronizedClass sc1 = new SynchronizedClass();
Thread t1 = new Thread(sc1);
Thread t2 = new Thread(sc1);
t1.start();
t2.start();
// join()方法:当前线程结束之后join()方法才返回,如果不试用join,打印出来的数据可能是0,1000,2000
t1.join();
t2.join();
System.out.println("scene1 share:" + share);
//share = 0;
//场景2,t3,t4线程执行各自获得实例sc2,sc3的锁,可能同时访问共享的静态资源share出现线程安全问题
SynchronizedClass sc2 = new SynchronizedClass();
SynchronizedClass sc3 = new SynchronizedClass();
Thread t3 = new Thread(sc2);
Thread t4 = new Thread(sc3);
t3.start();
t4.start();
t3.join();
t4.join();
System.out.println("scene2 share:" + share);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
上面例子中:当前线程的锁是实例对象instance,java的线程同步锁可以是任意对象。注意:如果一个线程正在访问一个实例instance的synchronized方法A时,其他线程不能访问该实例的其他synchronized方法,因为一个对象实例只有一把锁,但其他线程可访问该实例的非synchronized方法。当然,如果其他线程要访问另外一个实例instance1的任何方法都是不受限制的,但是如果他们访问的是共享数据那么就会出问题,例如上面例子场景2中的静态变量share,这时候就需要使用②进行修饰。
②修饰静态方法:作用于当前类对象,进入同步代码前要获得当前类(class)对象的锁。注意:两个线程可以同时分别访问static synchronized修饰的方法和non-static synchronized修饰的方法,因为他们一个是类对象的锁,一个是实例对象的锁,但是此时两个方法如果同时操作共享数据,也可能出现线程安全问题。例如上面例子中两个线程分别同时访问:increase4ClassLock()和increase()方法,就会出现线程安全问题。
③修饰代码块:指定加锁对象,给指定对象家锁,进入同步代码块前要获取指定对象的锁(可以是类对象,也可以是实例对象),样例如下:
static SynchronizedClass inst1 = new SynchronizedClass();
public void increase2MethodBlock1(){
//使用实例锁,获取当前对象实例的锁,inst1也可用this(synchronized (inst1))
synchronized (inst1)
{
share++;
}
}
public void increase2MethodBlock2(){
//使用类对象锁,获取当前class对象的锁
synchronized (SynchronizedClass.class)
{
share++;
}
}
5,sychronized实现原理
jvm中synchronized的同步是通过管程(Monitor)实现的,在java的堆内存中,一个对象实例信息是按照如下方式存储的:
如上如所示结构,java对象头里面有锁信息,锁信息里有一个指向monitor(管程)对象的指针,管程对象在java虚拟机中是使用OjbectMonitor类实现的,类结构大致如下:
ObjectMonitor(){
_count = 0; //计数器
_WaitSet = null;
_EntryList = null;
_owner = null; //指向拥有ObjectMonitor对象的线程
……
}
说明:
①多个线程进入同步代码时,会先进入_ EntryList队列,获取到monitor对象后修改owner指向为当前对象线程,计数器加1
②线程调用wait()方法进入_WaitSet队列,owner置为空,count减1
③当前线程执行完毕也会重置owner为空,复位变量
6,synchronized需要注意的点
①可重入性
一个线程在synchronized方法体内部可以调用另外一个synchronized方法,因为是同一个线程请求同一个锁。子类继承父类时,子类可通过重入锁调用父类的synchronized方法。
②在线程sleep时调用interrupt方法会打断阻塞并抛出异常(对阻塞线程进行中断操作):
③处于非阻塞状态的线程需要我们手动进行中断检测并结束程序:
如上②和③两种情况(阻塞/和线程运行时中断中断的操作方法是不一样的[前者要捕捉异常,后者要手动判断中断状态并做中断处理]),可用下面代码兼顾两种场景:
④线程状态转换
1)等待阻塞:等待阻塞(使用wait,notify/notifyAll方法进入和退出)(wait是Object类的方法)
2)锁定阻塞:synchronized同步锁的获得和释放
3)其他阻塞:sleep,join,IO操作等(sleep是Thread类的静态方法)
4)就绪状态获得时间片就进入运行态,时间片用完或yield操作回到就绪状态
5)sleep和wait的区别:sleep方法是Thread类的今天方法而wait是Object类的方法;sleep方法不会释放对象锁,sleep时其他线程任然不能访问同步块,而wait方法会释放对象锁。
⑤线程中断操作对正在等待获取锁的synchronized方法或者代码块是不起作用的:一个正在等待锁的线程要么获得锁继续执行,要么继续等待。
⑥wait(); notify(); notifyAll();方法必须放在synchronized中,否则会抛出IllegalMonitorStateException异常,因为:调用这几个方法必须获取管程(ObjectMonitor)对象,该对象放在对象头中,而synchronized可以获取到该对象。