在《Java并发编程实战》书中对线程安全给出了下面的定义:
当多个线程访问某个类时,这个类始终都能表现处正确的行为
下面介绍一下各种变量的线程安全性。
基础类型局部变量
基础类型的局部变量储存在栈内存中,也就是说,局部变量永远不会被多个线程共享。所以说所有的基础类型的局部变量都是线程安全的。
引用类型局部变量
由于引用类型变量的属性都储存在堆中,但是在一般情况下,别的线程是获取不到该对象的,只要该对象不能够被其他方法获取到,那么他就是线程安全的。
成员变量
由于一个对象的成员是储存在堆内存上的,如果有多个线程同时更新同一个对象的同一个成员变量,那么这种情况就不是线程安全的。
需要说明的是,只有多个线程同时访问同一个资源而且多个线程都对此资源进行了些写操作,才会产生线程安全的问题,多个线程同时对同一个资源进行读操作时不会产生线程安全问题的。
Java同步块(synchronized block)用来可以实现同步,可以通过这种方式来有效地避免线程安全问题的出现。
看一个例子:
public class Tester {
public static void main(String[] args) throws Exception {
Example example = new Example();
MyThread myThread = new MyThread(example);
MyThread myThread1 = new MyThread(example);
myThread.start();
myThread1.start();
}
}
class Example {
public synchronized void execute() throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(2000);
}
}
}
class MyThread extends Thread {
private Example example;
MyThread(Example example) {
this.example = example;
}
@Override
public void run() {
try {
example.execute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的代码中,为execute方法加上synchronized关键字之后,程序会先输出0-9,之后再输出一次0-9;如果去掉synchronized关键字之后,两个线程会同时会执行execute方法。
这说明,如果一个方法加上了synchronized关键字,那么该方法就是一个同步方法,如果一个对象有synchronized方法,某一时刻某个线程已经进入到了synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
现在将每个MyThread构造方法的参数改为两个对象,再次运行,此时即使加上了synchronized关键字,execute方法仍然是同时执行的。这是因为Java中每个对象都有一个monitor,当一个线程去访问某个对象的synchronized方法时,会将该对象上锁,这时,任何一个线程都无法去访问该对象的同步方法了,直到此方法执行完毕或者抛出异常,该对象就会把锁释放,这时其他的线程才可以访问该方法。
MyThread myThread = new MyThread(new Example());
MyThread myThread1 = new MyThread(new Example());
myThread.start();
myThread1.start();
当synchronized作用于静态方法时,由于静态方法并不是属于对象,而是属于类,其锁是当前类的Class对象锁,而访问非静态的synchronized方法时,是使用的当前实例的对象锁。
看一个例子:
public class Tester {
public static void main(String[] args) throws Exception {
MyThread myThread = new MyThread(new Example());
MyThread myThread1 = new MyThread(new Example());
myThread.start();
myThread1.start();
synchronized (Tester.class) {
}
}
}
class Example {
public synchronized static void execute() throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println("exe" + i);
Thread.sleep(2000);
}
}
}
class MyThread extends Thread {
private Example example;
MyThread(Example example) {
this.example = example;
}
@Override
public void run() {
try {
Example.execute();
} catch (InterruptedException e) {
e.printSzaitackTrace();
}
}
}
在synchronized修饰非静态方法时,如果对于不同的实例对象来说,拿到的锁也是不同的,但是对于static方法来说,即使对不同的实例操作,也是拿到的一个Class对象锁,所以对于上面的代码中,即使有两个不同的线程传入了两个不同的Example对象,但是两个线程依然会互相制约,必须先执行完一个再执行另外一个。
synchronized代码块写法:
synchronized (object) {
}
表示再某个线程在执行的时候会将object对象上锁(该对象可以任意指定)。
在第一种修饰实例方法给出的代码中,将synchronized方法修改为synchronized代码块。
public void execute() throws InterruptedException {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println("exe" + i);
Thread.sleep(2000);
}
}
}
上面传入的对象是Example对象本身,这时,该锁起到的作用和synchronized实例方法的作用是相同的。如果改为synchronized(new Object())
,这时由于锁是针对Object对象的一个实例的,所以,该锁就不会起到相应的作用了。
在第二种修饰静态方法给出的代码中,同样将synchronized静态方法修改为synchronized代码块.
public void execute() throws InterruptedException {
synchronized (Exmple.class) {
for (int i = 0; i < 10; i++) {
System.out.println("exe" + i);
Thread.sleep(2000);
}
}
}
上面传入的对象是Example的Class对象,这时,该锁起到的作用和synchronized实例方法的作用是相同的。同样的,如果传入其他的对象,该锁就不会起到相应的作用了。