架构师入门笔记一 初识线程关键字
本章节主要介绍线程的关键字 synchronized,volatile 的含义,使用方法,使用场景,以及注意事项。
线程安全
首先我们要了解线程安全的概念:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
要如何确保类能表现出正确的行为,这就需要关键字出马!
关键字
synchronized
synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
其工作原理:当一个线程想要执行synchronized修饰的方法,必须经过以下三个步骤
- step1 尝试获得锁
- step2 如果拿到锁,执行synchronized代码体内容
- step3 如果拿不到锁,这个线程就会不断地尝试获得这把锁,直到拿到为止。这个过程可能是多个线程同时去竞争这把锁(锁竞争的问题)。
注*(多个线程执行的顺序是按照CPU分配的先后顺序而定的,而并非代码执行的先后顺序)
使用方法介绍:
synchronized 重入
在使用synchronized时,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
public class MySyncReentrant {
/**
* 重入调用
*/
private synchronized void method1() {
System.out.println("^^^^^^^^^^^^^method1");
method2();
}
private synchronized void method2() {
System.out.println("-----------------------method2");
method3();
}
private synchronized void method3() {
System.out.println("********************method3");
}
public static void main(String[] args) {
final MySyncReentrant mySyncReentrant = new MySyncReentrant();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
mySyncReentrant.method1();
}
}, "reentrant");
thread.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
SunClass sunClass = new SunClass();
sunClass.sunMethod();
}
});
thread2.start();
}
/**
* 有父子继承关系的类,如果都使用了synchronized关键字,也是线程安全的。
*/
static class FatherClass {
public synchronized void fatherMethod(){
System.out.println("fatherMethod....");
}
}
static class SunClass extends FatherClass{
public synchronized void sunMethod() {
System.out.println("sunMethod....");
this.fatherMethod();
}
}
}
synchronized 代码块:
如果被修饰的方法执行需要很长时间,线程之间等待的时间就会很长,所以将synchronized 修饰在代码块上是可以优化执行时间。(这也叫减少锁的粒度)
synchronized (this) {} , 可以是 this(对象锁) class(类锁) Object lock = new Object(); 任何对象锁。
import java.util.concurrent.atomic.AtomicInteger;
public class CodeBlockLock {
// 对象锁
private void thisLock () {
synchronized (this) {
System.out.println("this 对象锁!");
}
}
// 类锁
private void classLock () {
synchronized (CodeBlockLock.class) {
System.out.println("class 类锁!");
}
}
// 任何对象锁
private Object lock = new Object();
private void objectLock () {
synchronized (lock) {
System.out.println("object 任何对象锁!");
}
}
// 字符串锁,注意String常量池的缓存功能
private void stringLock () {
synchronized ("string") { // new String("string")
try {
for(int i = 0; i < 3; i++) {
System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 字符串锁改变
private String strLock = "lock";
private void changeStrLock () {
synchronized (strLock) {
try {
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");
strLock = "changeLock";
Thread.sleep(5000);
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final CodeBlockLock codeBlockLock = new CodeBlockLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.thisLock();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.classLock();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.objectLock();
}
});
thread1.start();
thread2.start();
thread3.start();
// 如果字符串锁,用new String("string") t4,t5线程是可以获取锁的,如果直接使用"string" ,若锁不释放,t5线程一直处理等待中
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.stringLock();
}
}, "t4");
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.stringLock();
}
}, "t5");
thread4.start();
thread5.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 字符串变了,锁也会改变,导致t7线程在t6线程未结束后变开始执行,但一个对象的属性变了,不影响这个对象的锁。
Thread thread6 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.changeStrLock();
}
}, "t6");
Thread thread7 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.changeStrLock();
}
}, "t7");
thread6.start();
thread7.start();
}
}
运行结果:
this 对象锁!
class 类锁!
object 任何对象锁!
thread : t4 stringLock !
thread : t4 stringLock !
thread : t4 stringLock !
thread : t5 stringLock !
thread : t5 stringLock !
thread : t5 stringLock !
thread : t6 changeLock start !
thread : t7 changeLock start !
thread : t6 changeLock end !
thread : t7 changeLock end !
注* 给String的常量加锁,容易会出现死循环的情况。 如果加锁的字符串变了,锁也会变。若一个对象的属性变了,是不影响这个对象的锁。static + synchronized 一起使用 是类级别的锁
synchronized 异常:
synchronized 遇到异常后,自动释放锁,让其他线程调用。如果第一个线程在执行任务时,因为异常导致业务逻辑未能正常执行。后续的线程执行的任务也都是异常的。所以在编写代码时一定要考虑周全
同步与异步
同步的概念就是共享,其目标就是为了线程安全(线程安全的两个特性:原子性和可见性),A线程获取对象的锁,若B线程想要执行synchronized方法,就需要等待,这就是同步。
异步的概念就是独立,A线程获取对象的锁,若B线程想要执行非synchronized方法,是无需等待的,这就是异步。可以参考ajax请求。
public class SyncAndAsyn {
private synchronized void syncMethod() {
try {
System.out.println(Thread.currentThread().getName() + " synchronized method!");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 若次方法也加上了 synchronized,就必须等待t1线程执行完后,t2才能调用
private void asynMethod() {
System.out.println(Thread.currentThread().getName() + " asynchronized method!");
}
public static void main(String[] args) {
final SyncAndAsyn syncAndAsyn = new SyncAndAsyn();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
syncAndAsyn.syncMethod();
}
}, "t1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
syncAndAsyn.asynMethod();
}
}, "t2");
thread1.start();
thread2.start();
}
}
volatile
volatile关键字不具备synchronized关键字的原子性(同步)其主要作用就是使变量在多个线程中可见。
原理图:
在java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝。当线程执行时,他在自己的工作内存区中操作这些变量。
为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到他自己的所在的工作内存区中。当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
而volatile的作用就是强制线程到主内存里面去读取变量,而不去线程工作内存区里面读,从而实现了多个线程间的变量可见。也就是满足线程安全的可见性。
可见性:(被volatile修饰的变量,线程执行引擎是直接从主内存中读取变量的值)
public class VolatileThread extends Thread{
// 如果不加 volatile,会导致 "thread end !" 一直没有打印,
private volatile boolean flag = true;
@Override
public void run() {
System.out.println("thread start !");
while (flag) {
}
System.out.println("thread end !");
}
public static void main(String[] args) {
VolatileThread thread = new VolatileThread();
thread.start();
try { // 等线程启动了,再设置值
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.setFlag(false);
System.out.println("flag : " + thread.isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
volatile 不具备原子性:(多线程之间不是同步的,存在线程安全,从下面的例子中可以得知:如果是同步的,最后一次打印绝对是1000*10 。为了弥补这个问题,可以考虑使用atomic类的系类对象)
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile关键字不具备synchronized关键字的原子性(同步)
*/
public class VolatileNoAtomic extends Thread{
// 多次执行程序,会发现最后打印的结果不是1000的整数倍.中途打印不是1000的整数倍,可能是因为System.out打印的延迟造成的
// 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[10];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
// 执行10个线程
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
其实atomic类 并非完美,它也只能保证自己方法是原子性,若要保证多次操作也是原子性,就需要synchronized的帮忙(若不用synchronized修饰,打印的结果中会出现非10倍数的信息,需多次执行才能模拟出来)
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicUse {
private static AtomicInteger count = new AtomicInteger(0);
//多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性 .
/**synchronized*/
public 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());
}
})); // 添加100个线程
}
for(Thread t : ts){
t.start();
}
}
}
以上便是初识线程关键字的内容,方便自己以后查阅,也希望对读者有些帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。