在上一篇博客主要是对多线程的一些基础知识进行了描述,这一篇主要说对象以及变量的并发访问。
一、线程安全
概念:当多个线程访问某一个类时,这个类时钟能表现出正确的行为,那么这个类就是线程安全的。
先说明一下当有多个线程时,他们执行的顺序是不确定的,和代码的先后顺序无关,也就是说线程二可能比线程一先执行。
为了解释线程安全的概念,先给出线程不安全的例子:
public class MyThread extends Thread{
private int count = 5 ;
//synchronized加锁
public void run(){
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
public static void main(String[] args) {
/**
* 分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的),
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
运行一下看结果:
非线程安全会产生“脏读”的后果,这是因为多个线程共同访问同一个对象的实例变量,此时线程访问变量的方式是异步的,为了·放置非线程安全,就必须使这些线程为同步的方式。
二、synchronized关键字
1、多个线程访问同一个变量
synchronized:这个关键字可以在任何对象和方法上加锁,而加锁的这部分代码被称为互斥区,或者是“临界区”
在上面的例子中,如果想要获得正确的结果,此时只需要使用synchronized关键字去修饰这个方法,此时多个线程访问MyThread线程的时候,以排队的形式进行处理,一个线程想要执行synchronized修饰的方法里面的代码,首先尝试获得锁,如果拿到锁,执行synchronized代码体内容,拿不到锁,这个线程就会不断地尝试去获得这把锁,一直到拿到为止,而且是多个线程同时去竞争这把锁。
看例子,在上面的线程中对run方法加锁
//synchronized加锁
public synchronized void run(){
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
测试代码不变,看结果:
2、多个对象多个锁
多个线程,每一个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。这点要和上面的进行曲别开,上面的例子中,多个线程去访问同一个变量,也就是只有一个synchronized修饰的对象,一个线程拿到这个对象之后,其他的线程必须等待,但是下面这个例子不是,在main方法中创建了两个synchronized修饰的对象。也就是创建了两把锁。
看例子再解释:
下面这个例子:在两个线程中分别去访问printNum方法,如果tag=a,那么num=100输出,否则tag=b,num=200
public class MultiThread {
private int num = 0;
/** static */
public synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//注意观察run方法输出顺序
public static void main(String[] args) {
//俩个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
说明:关键字synchronized取得的锁都是对象锁,而不是把一段代码当锁,所以代码中哪一个线程先执行synchronized关键字修饰的方法,哪一个线程就持有该方法所属对象的锁,
看结果:
解释这个例子:关键字synchronized取得的锁都是对象锁,而不是把一段代码当作锁,所以示例代码中的哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,现在有两个对象,线程获得的是两个不同的锁,他们互不影响。有一种情况表示的是相同的锁,也就是静态方法加上synchronized关键字,
比如上面的printNum更改:注意num也要换成静态变量
/** static */
public static synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
再看结果:
3、对象锁的同步和异步问题
同步的概念就是共享,如果不是共享的资源,那么就没必要进行同步,下面一个例子看一下对象锁的同步问题:
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** synchronized */
public void method2(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
final MyObject mo = new MyObject();
/**
* 分析:
* t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
* t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mo.method1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mo.method2();
}
},"t2");
t1.start();
t2.start();
}
}
4、synchronized锁重入
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时候,当一个线程得到了一个对象的锁之后,再次请求此对象时是可以再次得到该对象的锁。下面举例子说明:
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1..");
method2();
}
public synchronized void method2(){
System.out.println("method2..");
method3();
}
public synchronized void method3(){
System.out.println("method3..");
}
public static void main(String[] args) {
final SyncDubbo1 sd = new SyncDubbo1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
这段代码很简单,直接看结果
再看一个:
public class SyncDubbo2 {
static class Main {
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Main print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub(){
try {
while(i > 0) {
i--;
System.out.println("Sub print i = " + i);
Thread.sleep(100);
this.operationSup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Sub sub = new Sub();
sub.operationSub();
}
});
t1.start();
}
}
看结果:
5、出现异常,锁自动释放
当一个线程执行的代码出现异常的时候,会将其持有的锁自动释放
public class SyncException {
private int i = 0;
public synchronized void operation(){
while(true){
try {
i++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , i = " + i);
if(i == 20){
Integer.parseInt("a");
throw new RuntimeException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SyncException se = new SyncException();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
},"t1");
t1.start();
}
}
看结果:
6、synchronized代码块
(1)减小锁的粒度
使用synchronized声明的方法在某些情况下是有弊端的,比如说A线程调用同步的方法执行一个很长的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况下面可以使用synchronized代码块去优化代码的执行时间,也就是通常所说的减小锁的粒度。
public class Optimize {
public void doLongTimeTask(){
try {
System.out.println("当前线程开始:" + Thread.currentThread().getName() + ", 正在执行一个较长时间的业务操作,其内容不需要同步");
Thread.sleep(2000);
synchronized(this){
System.out.println("当前线程:" + Thread.currentThread().getName() + ", 执行同步代码块,对其同步变量进行操作");
Thread.sleep(1000);
}
System.out.println("当前线程结束:" + Thread.currentThread().getName() +", 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final Optimize otz = new Optimize();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t2");
t1.start();
t2.start();
}
}
看结果:
(2)不要使用string的常量加锁
这个标题的原因是因为会出现死循环问题。看一个例子
public class StringLock {
public void method() {
//new String("字符串常量")
synchronized ("字符串常量") {
try {
while(true){
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t2");
t1.start();
t2.start();
}
}
看执行结果:
永远不会执行t2,这是因为synchronized 对字符串常量加锁之后,一直处于死循环状态,但是如果我们对字符串对象加锁,可以避免出现死循环问题。
(3)锁对象的改变问题
当使用一个对象进行加锁的时候,如果对象本身发生了改变,那么持有的锁就会不同。如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。看一个例子
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
//在这里对锁对象发生了改变
lock = "change lock";
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
看结果:改变之后t2也能进入。
(4)死锁问题
死锁问题指的是双方持有对方的锁。
三、volatile关键字
概念:volatile关键字的主要作用是使变量在多个线程之间可见。
在java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝。当线程执行的时候,它在自己的工作内存区操作这些变量。为了存取一个共享的变量,一个线程通常先获取锁并清除它的内存工作区。把这些变量从所有线程的共享内存区中正确的装入到他自己所在的工作内存区中,当线程解锁时保证该工作内存区中变量的值写回到共享内存区中。
volatile的作用是强制线程从主内存中去读取变量,而不是去线程工作的内存区去读取,从而实现了多个线程之间的变量可见,也就是满足线程安全的可见性。下面看个例子:
public class RunThread extends Thread{
private volatile boolean isRunning = true;
private void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run(){
System.out.println("进入run方法..");
int i = 0;
while(isRunning == true){
//System.out.println("run方法:"+isRunning);
}
System.out.println("线程停止");
}
public static void main(String[] args) throws InterruptedException {
RunThread rt = new RunThread();
rt.start();
Thread.sleep(1000);
rt.setRunning(false);
System.out.println("isRunning的值已经被设置了false");
}
}
看结果:
(1)volatile的非原子性
volatile关键字虽然拥有多个线程之间的可见性,但是缺不具备原子性,但是不会造成阻塞,不具备synchronized的同步功能。
看个例子说明没有2原子性
public class VolatileNoAtomic extends Thread{
private static volatile int count;
//private static AtomicInteger count = new AtomicInteger(0);
private static void addCount(){
for (int i = 0; i < 1000; i++) {
count++ ;
//count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
看结果:
本来应该是10000的但是只有9960,说明其没有原子性,输出8960的时候有一些自增操作没有实现。
(2)volatile实现原子性
public class AtomicUse {
private static AtomicInteger count = new AtomicInteger(0);
//多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性
/**synchronized*/
public synchronized int multiAdd(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4); //+10
return count.get();
}
public static void main(String[] args) {
final AtomicUse au = new AtomicUse();
List<Thread> ts = new ArrayList<Thread>();
for (int i = 0; i < 100; i++) {
ts.add(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(au.multiAdd());
}
}));
}
for(Thread t : ts){
t.start();
}
}
}
要实现原子性建议使用atomic类的系列对象,支持原子性操作。
到此结束,下一篇看线程之间的通信