线程安全概念:当多个线程访问一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
1.可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
2.一般状况下,关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁。
3.在静态方法上加上synchronized关键字,表示锁定.class类(或者
synchronized(xxx.class){
//代码块
}
),同时一间段内只能一个线程访问该类的所有对象。
4.synchronized(同步)和Asynchronized(异步)的问题:
概念:异步双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送,所以在发送的信息中就要有提示接收方开始接收的信息,如开始位,同时在结束时有停止位。
多线程环境下,加synchronized关键字就是同步,不加synchronized就是异步,并发执行就是异步的体现。
例子:
public class AsyncAndSync{
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());
}
/**
* t1线程先持有AsyncAndSync对象的锁,此时正在执行method1方法,如果method2没有被添加
* 关键字synchronized,那么t2线程就会同步和t1线程执行,因为访问ass对象中的method2方
* 法不需要锁,t2线程直接访问就可以了。如果method2方法也添加了关键字synchronized,那么t1和t2
* 就会顺序执行,因为t1已经获取了对象锁了,方法锁等同对象锁,对象锁是针对成员变量或成员
* 方法而进行锁定的,没有被synchronized修饰的成员变量或方法是不会被对象锁锁定的,同一个
* 对象同一时间段内只能有一把锁。
*/
public static void main(String[] args){
final AsyncAndSync ass = new AsyncAndSync();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ass.method1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ass.method2();
}
},"t2");
t1.start();
t2.start();
}
}
显示结果:method2不加锁:t1和t2同时输出,method2加锁:t1和t2先后输出
5.如果是静态方法加上synchronized关键字就会变成类锁,无论有多少个对象进行访问,都得串行执行。
6.脏读 实例:
//业务整体需要使用完整的synchronized,保持业务的原子性。
public class DirtyRead {
private String username = "ljj";
private String password = "123456";
public synchronized void setValue(String username, String password){
this.username = username;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果:username = " + username + " , password = " + password);
}
//synchronized
public void getValue(){
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
/**
* 在getValue()没有使用synchronized关键字的时候,这样执行会导致数据脏读,t1线程在设置新值
* 的时候,整个设置过程需要2s,而在1s后main线程就开始读取数值了,此时读取的数值为对象属性的
* 旧数值,则为脏读。getValue()方法加上synchronized则课解决此问题
*
public static void main(String[] args) throws Exception{
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("ljq", "obj");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
数据一致性读 Oracle数据库中规避脏读的手段的实例:
7.最简单的同步锁重入和死锁:
public class SyncReentry {
public synchronized void method1(){
System.out.println("method1..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
method2();
}
public synchronized void method2(){
System.out.println("method2..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
method3();
}
public synchronized void method3(){
System.out.println("method3..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 锁的重入,如果在method3中调用method1就会形成最简单的死锁,
* 在设计程序的时候应该避免双方同时持有锁的状况
*/
public static void main(String[] args) {
final SyncReentry sr = new SyncReentry();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sr.method1();
}
});
t1.start();
}
}
8.锁的重入2:子类方法被加锁后调用父类方法,父类方法同样要加锁,不然保证不了线程的安全性。
9.锁对象改变问题:在对象锁中,不要改变字符串对象的值,当锁对象被改变了,也就是锁改变了,当前线程的锁就会被释放,加入是t1线程执行以下的代码块,t1在获取锁后将锁的对象改变就会导致锁对象被释放,t2线程就会获取锁,锁的对象为t1线程改变后的对象。
private String lock = “lock”;
synchronized (lock) {
lock = “change lock”;
}
10.同一个类中,不同的方法,不同的锁->只有一个对象被三个线程分别调用:
public class ObjectLock {
public void method1(){
synchronized (this) { //对象锁
...
}
}
public void method2(){ //类锁
synchronized (ObjectLock.class) {
...
}
}
private Object lock = new Object();
public void method3(){ //任意对象锁
synchronized (lock) {
...
}
}
public static void main(String[] args) {
final ObjectLock objLock = new ObjectLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method2();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method3();
}
});
t1.start();
t2.start();
t3.start();
}
}
这种状况就是三个线程分别获得三把不同的锁,执行顺序不固定。
11.尽量减小锁的粒度,提高程序的性能,类锁是最耗性能的,对象锁其次,可以的话尽量将锁用在代码块身上,如下例,假设t1和t2线程先后调用Particle实例化的一个对象,假设synchronized锁住的是方法,t2就会等待t1释放锁才能进到方法体内,假如是按照如下的锁住块代码体,t2就会在a位置等待t1释放锁,事先就执行了一部分不需要锁锁住的代码,整个程序的运行速度等待了提升,性能得到了提高。
public class Particle{
void timeTask(){
…
//a位置
synchronized(this){
…
}
//b位置
…
}
}
12.不要用字符串常量作为锁,应该通过new String(“字符串常量”)的方式来,常量池的东西是全局的,任何一个地方出现了相同的代码,很容易导致问题,通过new 的方式就可以规避该类问题。另一方面,可能存在竞争不合法的问题,并不是程序不正确的问题。
synchronized (new String(“字符串常量”)) {…}