1.概要
- 线程安全:多个线程同时访问公共对象或者同一个对象时,采用了加锁的机制,对公共数据进行保护,直到线程对该数据使用完。
- 非线程安全:多个线程同时访问公共对象或者同一个对象时,发生数据不一致或者数据污染
- 脏读:读到的数据其实是被更改过的,数据不一致或者数据污染。
2.Synchronized方法与锁对象
-
synchronized
1.监视器:每个对象都有一个监视器(monitor),它允许每个线程同时互斥和协作,就像每个对象都会有一块受监控的区域
(数据结构),当线程执行需要取到监控区域的数据时,首先验证是否有线程拥有监视器,已有线程拥有监视器则进入监视
器的monior entry list进行等待Thread.state:BLOCKED,直到释放退出监控区域且释放锁。以这种FIFO的方式等待。
- 对象锁:每个对象在堆内存中的头部都会维持一块锁区域,任何线程要同步执行对象数据都会放入监视器且都必须获锁。
一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的
对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了。
synchronized工作机制是这样的:Java中每个对象都有一把锁与之相关联,
锁控制着对象的synchronized代码。一个要执行对象的synchronized代码
的线程必须先获得那个对象的锁。
-
synchronized 同步方法
一个对象只有一把对象锁
synchronized获取的是对象锁,保证线程顺序进入对象方法。
public class test {
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
执行结果:可以看出线程是并行运行的。start:A
start:B
end
end
在方法加入同步synchronized关键字,执行结果:线程同步顺序执行
start:A
end
start:B
end
锁重入:关键字synchronized拥有锁重入的功能,当一个线程得到对象锁后在当前线程能够再次获得此对象锁,这说明一个
线程在得到对象锁后可以无限制的获得对象锁。
public class test {
public void methodA(){
System.out.println("非synchronized方法");
}
public synchronized void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
methodB();
} catch (Exception e) {
// TODO: handle exception
}
}
public synchronized void methodB() throws InterruptedException{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
执行结果:自己可以再次获取自己的内部对象锁,当线程获取到对象锁后在其内部还可以获得对象锁,如果锁不可重入的话
就很容易造成死锁,因为外部对象锁还未释放,导致在内部永远获取不到对象锁,线程永远处于等待。
start:A
end
start:B
end
出现异常,锁会自动释放:当一个线程执行代码是出现异常时,会自动释放所持有的对象锁。
同步不具有继承性:当父类方法进行同步,子类重写该方法,子类方法不具有同步性,需要添加synchronized关键字。
-
synchronized同步代码块
用synchronized同步方法是有弊端的,当方法某一条语句执行时间过长,就会导致其他线程需要等待较长时间。所以同步 代码块可以相对提高效率。
- 同步代码块的使用
synchronized需要依赖于对象锁,同步代码块是需要一个锁对象,可以是当前对象(this),一般系统并发量很高不采用当
前对象,而采用任意其他一个对象,不然造成大量线程等待在该对象。
public class test {
public void methodA(){
System.out.println("非synchronized方法");
}
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
synchronized (this) {
methodB();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public synchronized void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
执行结果:线程A、B并发进入方法,但有一个线程等待在同步代码块前。start:A
start:B
end
end
任意对象锁:在同步代码块不取this(当前对象)锁,而采用任意对象锁,这样的好处是当有很多synchronized同步方法时
,如果用this对象锁,会造成阻塞,而采用任意锁,其他同步块则不会造成阻塞。
public class test {
private Object obj = new Object();
public void methodA(){
System.out.println("非synchronized方法");
}
public void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
synchronized (this) {
methodB();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public synchronized void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public void methodC(){
synchronized (obj) {
try {
System.out.println("start:"+Thread.currentThread().getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();//执行methodC
}
}
执行结果:线程A使用当前this对象锁同步代码块,线程B能够同步执行OBJ对象锁进行同步代码块
start:A
start:B
end
同步代码块解锁无限等待问题:同步方法获得的是当前对象的对象锁,一单其中一个同步方法陷入死循环,该对象的其他同
步方法都无限等待,所以同步需要同步的部分代码块且使用任意对象锁。
-
synchronized同步静态方法
关键字synchronized还可以修饰static方法,如果这样写,那是对当前的.java文件的class对象进行加锁。
- 与实例方法取得不同的对象锁
Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。这项信息纪录了
每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。
Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。
public class test {
private Object obj = new Object();
public synchronized void methodA(){
System.out.println("start not static :"+Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("static end");
}
public synchronized static void testSynchronized(){
try {
System.out.println("start:"+Thread.currentThread().getName());
methodB();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public static void methodB() throws Exception{
Thread.sleep(5000);
System.out.println("end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
执行结果:两个线程获取的是不同的对象锁,一个是test.classs对象锁,一个是test对象锁
start not static :A
start:B
static end
end
- String常量池类型锁
在JVM中String有常量池缓存的特性
什么事常量池?:
常量池(constant
pool)在编译期间被指定,并被保存在已编译的.class文件当中,用于存储关于类、方法、接口中 的常量,也包括字符串直接量。
String与常量池
String str1 = new String("abc")
String str2 = "abc";
上面是两种创建字符串的方式,看起来没有什么区别,但实则有很大区别
第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。
而第二种是先在栈中创建一个对String类的对象引用变量str2,然后通过符号引用去字符串常量池里找有没有"abc",如果没有
,则将"abc"存放进字符串常量池,并令str2指向”abc”,如果已经有”abc” 则直接令str2指向“abc”。
所以如果String str3 = “abc”;str2 和str3是同一对象,String str4 = “adcd”;str4和str2是不同的对象。
结论:如果使用String对象作为对象锁,必须要注意是否对象会改变;这就是String常量池带来的问题,一般不会用String做
为对象锁,而改用其他,比如 new Object()。
-
死锁
由于不同的线程都在等待永远不能被释放的锁,从而导致任务不能继续执行。在多线程中死锁是必须避免的,会导致线程的
假死。
public class test {
public synchronized void methodA(){
System.out.println("start:"+Thread.currentThread().getName());
try {
Thread.sleep(5000);
while(true){
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("static end");
}
public static void main(String[] args) {
test t = new test();
ThreadA threadA = new ThreadA(t);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(t);
threadB.setName("B");
threadB.start();
}
}
执行结果:程序会一直等待。
我们可以通过jstack 命令来查看jvm内线程状态,来找到死锁的地方。
-
volatile关键字
volatile关键字的主要作用是使变量在多个线程中可见。
强制从公共的堆栈中取得变量的值,而不是在线程的私有栈中取变量的值。
解决同步死循环:
public class RunThread implements Runnable{
private boolean isRun = true;
public void setIsRun(boolean flag){
this.isRun = flag;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(isRun == true){
try {
Thread.sleep(2000);
System.out.println("当前线程:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("end");
}
}
public class test {
public static void main(String[] args) {
RunThread runthread = new RunThread();
new Thread(runthread).start();
System.out.println("我要停止了");
runthread.setIsRun(false);
}
}
执行结果:上面代码运行在64bitJVM,-server模式程序陷入死循环。解决办法是使用volatile关键字,从公共堆栈中取得数
据,而不是线程私有栈中。
volatile非原子性
public class MyThread extends Thread{
volatile private static int count = 0;
public void run(){
addCount();
}
public void addCount(){
for(int i = 0;i<100;i++){
count++;
}
System.out.println(count);
}
}
主类:
public class test {
public static void main(String[] args) {
MyThread [] threadArr = new MyThread[100];
for(int i=0;i<100;i++){
threadArr[i] = new MyThread();
}
for(int i =0;i<100;i++){
threadArr[i].start();
}
}
}
执行结果
8500
8900
9100
9204
9298
9398
9498
9598
9698
9798
9898
9998
关键字volatile主要使用场合是在多线程中可以感知变量值更改了,并且可以获得最新的值,每次取值都是从共享内存中
取的数据,而不是从线程的私有内存中取得数据。
但如果修改实例变量,比如i++,这样的操作并不是一个原子操作,也是非线程安全的。表达式的步骤是:
1)从内存中取得i的值
2)计算i的值
3)将i的值写到主存中
如果在第二步计算值时,其他线程也在修改i的值,就会出现脏读。解决办法是使用synchronized,volatile只能保证从
每次从主存中取得i的值。所以说volatile并不具有原子性。而是强制数据的读写影响到主存中去。