当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
电影院卖票问题:
100张票四个窗口同时卖
public class Main {
public static void main(String[] args) {
SaleTickets st = new SaleTickets();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
Thread t4 = new Thread(st);
//三个窗口同事卖票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SaleTickets implements Runnable {
private int tickets = 100; //剩余十张票
@Override
public void run() {
while (true) {
if (tickets>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩" + tickets--);
}
}
}
}
/**
* Thread-3还剩8
* Thread-0还剩9
* Thread-2还剩10
* Thread-1还剩7
* Thread-3还剩6
* Thread-2还剩4
* Thread-0还剩3
* Thread-1还剩5
* Thread-0还剩0
* Thread-2还剩2
* Thread-1还剩-1
* Thread-3还剩1
*/
发现程序出现了个问题 各个窗口的剩余票数不同步 这种问题称为线程不安全
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:Java中提供了同步机制(synchronized)来解决。
通过加锁和解锁的操作,就能保证多条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized
关键字对一个对象进行加锁:
synchronized(lock) {
n = n + 1;
}
ynchronized
保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized
改写如下:
class SaleTickets implements Runnable {
private int tickets = 100; //剩余十张票
private Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (tickets>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩" + tickets--);
}
}
}
}
}
注意
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
它表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。
使用synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率。
使用synchronized
:
synchronized(lockObject) { ... }
。在使用synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁
我们经常会用到StringBuffer和StringBuilder,StringBuilder不是线程安全的,StringBuffer是线程安全的
/**
* 验证StringBuffer线程安全,如下,如果length==1000,则可证明
* @throws InterruptedException
*/
public static void testStringBuffer() throws InterruptedException {
StringBuffer sb = new StringBuffer();
for (int i=0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<1000; j++){
sb.append("a");
}
}
}).start();
}
Thread.sleep(100);
System.out.println(sb.length());
}
/**
* 主测试方法
* @param args
*/
public static void main(String[] args) {
try {
ThreadTest.testStringBuffer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过上面代码,我们开10个线程,每个线程循环1000次往StringBuffer对象里面append字符。理论上应该输出实例sb的字符串长度=10000,我们执行代码,实际输出也是10000,证明了StringBuffer是线程安全的。
/**
* 验证StringBuild线程不安全,如下,如果length!=1000,则可证明
* @throws InterruptedException
*/
public static void testStringBuild() throws InterruptedException {
StringBuilder sb = new StringBuilder();
for (int i=0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<1000; j++){
sb.append("a");
}
}
}).start();
}
Thread.sleep(100);
System.out.println(sb.length());
}
/**
* 主测试方法
* @param args
*/
public static void main(String[] args) {
try {
ThreadTest.testStringBuild();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过上面代码,我们开10个线程,每个线程循环1000次往StringBuilder对象里面append字符。理论上应该输出实例sb的字符串长度=10000,我们执行代码,实际输出<10000,证明了StringBuilder是线程不安全的。
StringBuilder和StringBuffer主要不同在于,StringBuffer的append、delete、replace、length等方法前都加了synchronized关键字保证线程安全,而StringBuilder没有,另外的区别在于StringBuffer有一个toStringCache的char数组,是用于记录最近一次toString()方法的缓存,任何时候只要StringBuffer被修改了这个变量会被赋值为null
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized
同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。