线程安全
简单的说,多个线程同时操作一个数据就有可能造成数据不一致,这就是线程不安全。
线程安全发生的前提:
1.在多线程环境中
2.存在共享数据
只要在这种情况下,多个线程同时修改一个共享数据就可能存在线程安全问题。
看一下demo代码
package test;
public class ThreadTest implements Runnable {
static int count=0;
public static void main(String[] args) {
ThreadTest tt=new ThreadTest();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
Thread t3=new Thread(tt);
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(10000);//确保所有线程执行完
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(count);
}
public void run()
{
int n=1000;
while(n>0)
{
count++;
n--;
}
}
}
运行结果
可以看到结果不是3000,是一个小于3000的数,这就出现了线程安全问题。
在java中线程有两种常见的解决线程安全问题的方法。
1.synchronized关键字
在java中每一个对象都可以是一个锁,这也是synchronized实现同步的基础。
1.修饰普通方法时,锁是当前的实例对象,也就是this对象。(也称对象锁)
2.修饰静态方法时,锁是当前类的字节码对象,也就是xxx.class对象。(也称类锁)
3.修饰同步方法块时,锁是括号内的对象,给指定的对象加锁。
在进入同步代码块时,首先就要获取锁!
synchronized锁是可重入的,可重入的意思就是同一个线程拥有某个对象的锁时,再次申请仍会获得该对象的锁,也就说synchronized锁是可重入的。
注意:synchronized是关键字,它不是锁!
代码演示
package test;
public class ThreadTest implements Runnable {
static int count=0;
public static void main(String[] args) {
ThreadTest tt=new ThreadTest();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
Thread t3=new Thread(tt);
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(10000);//确保所有线程执行完
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(count);
}
public synchronized void run()
{
int n=1000;
while(n>0)
{
count++;
n--;
}
}
}
结果为
和预期一样,线程安全,为3000.
2.ReetrantLock
ReetrantLock实现了lock接口,是一种比较常见的解决线程安全问题的锁,它也是可重入的。
先看代码演示
package test;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements Runnable {
static int count=0;
private static ReentrantLock reetrantLock=new ReentrantLock();
public static void main(String[] args) {
ReetrantLockDemo tt=new ReentrantLockDemo();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
Thread t3=new Thread(tt);
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(10000);//确保所有线程执行完
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(count);
}
public void run()
{
reetrantLock.lock();
int n=1000;
while(n>0)
{
count++;
n--;
}
reetrantLock.unlock();
}
}
结果为
接下来谈论reentrantLock的一些特性
加锁,解锁
公平锁,不公平锁
首先看加锁,解锁的源码
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
可以看出底层是sync对象调用的lock(),release()操作来完成的
private final Sync sync;
而Sync是一个用final修饰的私有成员变量,所以必须用构造函数初始化
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从这里就可以看出当你使用无参构造函数,或者传一个false的时候你创建的就是不公平锁,传一个true的参数时就是公平锁。
公平锁与不公平锁的竞争机制有区别。
当线程进入排队队列的时候公平锁与不公平锁没有区别,都是按顺序唤醒进程,但是当h锁刚被释放的时候,有一个新线程进来,它就会和排在head的线程竞争而不是直接进入排队队列,并且有可能胜利,所以就造成了不公平。而对于公平锁它会直接进入排队队列。
公平锁大部分时间没有必要使用,只有在业务必须要求公平的时候才使用,synchronized是不公平锁
关于可重入
reentrantLock里面有一个字段state,但线程占有锁的时候state加1,因为reentrantLock是可重入的,所以当同一个线程再次申请这个锁的时候,它能成功,此时state再次加1。释放锁时,也是同样释放成功state减1.。当state为0 的时候,它才会去队列里唤醒下一个线程。
synchronized和ReetrantLock的一些区别
- synchronized是关键字,ReetrantLock是类
- ReetrantLock能够实现比synchronized更加细粒的控制,比如公平性
- ReetrantLock可以对获取锁的等待时间进行设置 ,避免死锁
- ReetrantLock调用lock获取锁后,要用unlock解锁
- ReetrantLock可以获取各种锁的信息