您的位置:首页 > 编程语言 > Java开发

Java 多线程(二) synchronized关键字

2018-01-25 17:19 357 查看

1.概要

线程安全:多个线程同时访问公共对象或者同一个对象时,采用了加锁的机制,对公共数据进行保护,直到线程对该数据使用完。

非线程安全:多个线程同时访问公共对象或者同一个对象时,发生数据不一致或者数据污染

脏读:读到的数据其实是被更改过的,数据不一致或者数据污染。

2.Synchronized方法与锁对象

[b]synchronized[/b]

1.监视器:每个对象都有一个监视器(monitor),它允许每个线程同时互斥和协作,就像每个对象都会有一块受监控的区域

 (数据结构),当线程执行需要取到监控区域的数据时,首先验证是否有线程拥有监视器,已有线程拥有监视器则进入监视

 器的monior entry list进行等待Thread.state:BLOCKED,直到释放退出监控区域且释放锁。以这种FIFO的方式等待。

对象锁:每个对象在堆内存中的头部都会维持一块锁区域,任何线程要同步执行对象数据都会放入监视器且都必须获锁。
 一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的

 对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了。

synchronized工作机制是这样的:Java中每个对象都有一把锁与之相关联,

锁控制着对象的synchronized代码。一个要执行对象的synchronized代码

的线程必须先获得那个对象的锁。

[b]synchronized 同步方法[/b]

 一个对象只有一把对象锁

 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关键字。

[b]synchronized同步代码块[/b]

 用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

 同步代码块解锁无限等待问题:同步方法获得的是当前对象的对象锁,一单其中一个同步方法陷入死循环,该对象的其他同

步方法都无限等待,所以同步需要同步的部分代码块且使用任意对象锁。

[b]synchronized同步静态方法[/b]

 关键字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()。

[b]死锁[/b]

 由于不同的线程都在等待永远不能被释放的锁,从而导致任务不能继续执行。在多线程中死锁是必须避免的,会导致线程的

 假死。

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内线程状态,来找到死锁的地方。

[b]volatile关键字[/b]

 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并不具有原子性。而是强制数据的读写影响到主存中去。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息