Java中的实例锁(Synchronized)和类锁(Static Synchronized)
锁的作用
对于多线程同时访问共享变量时,就会产生线程安全问题。锁的作用就是对共享的变量进行加锁,当有线程在访问变量时,其他线程必须要等锁释放才可以访问,解决线程安全问题。
线程安全问题
直接上代码,下面是模拟10个线程对同时卖票的场景。
public class ThreadSecurtiy implements Runnable{
int ticket = 100;
@Override
public void run() {
sellTicket();//设置线程任务是调用addSalary接口
}
public void sellTicket(){
while (ticket > 0){
try {
Thread.sleep(100);//睡眠是为了增加线程问题出现的概率,方便演示
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
}
}
public static void main(String[] args) {
ThreadSecurtiy ts = new ThreadSecurtiy();//创建一个ts对象
for(int i = 0;i < 10;i++){//通过ts创建了10个线程,这10个线程对同一个对象ts的salary进行访问
new Thread(ts).start();
}
}
}
输出结果是(当然结果太多了。后面就不贴了。)
Thread-5正在卖96张票
Thread-0正在卖95张票
Thread-3正在卖96张票
Thread-1正在卖96张票
Thread-2正在卖96张票
Thread-9正在卖94张票
Thread-8正在卖93张票
Thread-7正在卖92张票
Thread-4正在卖90张票
Thread-6正在卖91张票
Thread-5正在卖87张票
从结果中可以看出,如果十个线程不加以控制,会出现各种问题,比如96号票卖了多次,100号票根本就没有卖。
实例锁
实例锁很简单,就是在刚才的addSalary前加上sychroinzed关键字,但是这个实例锁,只对一个实例起作用,比如上面我们只创建了一个实例ts。
其他地方都一样哦,只贴sellTicket()方法了。
public synchronized void sellTicket(){
while (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
}
}
结果是
Thread-0正在卖99张票
Thread-0正在卖98张票
Thread-0正在卖97张票
Thread-0正在卖96张票
Thread-0正在卖95张票
Thread-0正在卖94张票
Thread-0正在卖93张票
Thread-0正在卖92张票
Thread-0正在卖91张票
结果分析:发现只有thread0在卖票,这是因为线程0占有了锁不会释放,一只到run方法结束才释放,所以线程安全问题是解决了。但是并行度也降低了。
那么如果加了实例锁,现在创建了两个对象呢?按照道理来说,肯定是还有线程安全问题的!代码
public class ThreadSecurtiy implements Runnable{
int ticket = 100;
@Override
public void run() {
sellTicket();//设置线程任务是调用addSalary接口
}
public synchronized void sellTicket(){
while (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
}
}
public static void main(String[] args) {
ThreadSecurtiy ts = new ThreadSecurtiy();//创建一个ts对象
ThreadSecurtiy ts1 = new ThreadSecurtiy();//创建另一个对象
new Thread(ts).start();
new Thread(ts1).start();
}
}
注意这里主函数中创建了两个对象,运行的结果是:
Thread-1正在卖99张票
Thread-0正在卖99张票
Thread-0正在卖98张票
Thread-1正在卖98张票
Thread-1正在卖97张票
Thread-0正在卖97张票
Thread-1正在卖96张票
结果分析:两个对象在堆内存中创建了不同的实例,也就是两份salary所以肯定会出现如上的情况。这也是为什么实例锁只能锁一个对象的原因!因为两个对象,锁也是两个,堆内存是线程共享的!
类锁
看这个名字就知道,它的锁的范围是针对类的,就是这个类的所有对象,都能保证线程同步。类锁就是在sellTicket方法前用static synchroinzed关键字修饰。直接上代码
public class ThreadSecurtiy implements Runnable{
static int ticket = 100;
@Override
public void run() {
sellTicket();//设置线程任务是调用addSalary接口
}
public static synchronized void sellTicket(){
while (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
}
}
public static void main(String[] args) {
ThreadSecurtiy ts = new ThreadSecurtiy();//创建一个ts对象
ThreadSecurtiy ts1 = new ThreadSecurtiy();//创建另一个对象
new Thread(ts).start();
new Thread(ts1).start();
}
}
结果是:
Thread-0正在卖99张票
Thread-0正在卖98张票
Thread-0正在卖97张票
Thread-0正在卖96张票
Thread-0正在卖95张票
Thread-0正在卖94张票
Thread-0正在卖93张票
Thread-0正在卖92张票
结果分析:加上类锁之后,即使创建了两个线程对象,仍然可以保证线程的同步,说明类锁起了作用。原因是什么呢?以下为我个人理解哈:类锁加上static关键字,会导致这个锁和类最先加载,并且都存放在方法区,如果不加static,锁会和对象绑定,创建在堆区。所以不管创建多少个对象,它们对应的锁都是一个!
这就是用静态和不用静态的区别,也不知道对不对。
其他解决线程安全问题的方法
同步代码块
还是上面的代码,在会被共享的变量周围加上同步代码块:
public class ThreadSecurtiy implements Runnable{
int ticket = 100;
@Override
public void run() {
sellTicket();//设置线程任务是调用addSalary接口
}
public void sellTicket(){
while (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
}
}
}
public static void main(String[] args) {
ThreadSecurtiy ts = new ThreadSecurtiy();//创建一个ts对象
new Thread(ts).start();
new Thread(ts).start();
new Thread(ts).start();
}
}
部分结果:
Thread-0正在卖99张票
Thread-1正在卖98张票
Thread-2正在卖97张票
Thread-1正在卖96张票
Thread-0正在卖95张票
Thread-2正在卖94张票
Thread-0正在卖93张票
Thread-1正在卖92张票
Thread-2正在卖91张票
结果分析:可以看出,没有线程安全问题,因为在会对共享的变量的周围加上了同步代码块,同步代码块的参数就是一个对象,随便什么对象都行,所以我写了this。原理就是,线程修改ticket的时候,会把这个锁给拿走,其他线程访问的时候没有这个锁,就只能阻塞,当线程0用完了之后会归还锁,线程1拿到锁,对ticket操作。
ReentrantLock锁
它实现了lock接口,常用的方法是里面的.lock()和unlock(),也很好理解,和同步代码块差不多!注意使用的时候要导包!
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSecurtiy implements Runnable{
int ticket = 100;
@Override
public void run() {
sellTicket();//设置线程任务是调用addSalary接口
}
public void sellTicket(){
Lock lk = new ReentrantLock();
while (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lk.lock();
if(ticket > 0) ticket--;//当票数大于0的时候,就可以卖票
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票");
lk.unlock();
}
}
public static void main(String[] args) {
ThreadSecurtiy ts = new ThreadSecurtiy();//创建一个ts对象
new Thread(ts).start();
new Thread(ts).start();
new Thread(ts).start();
}
}
运行结果:
Thread-0正在卖99张票
Thread-1正在卖98张票
Thread-2正在卖97张票
Thread-0正在卖96张票
Thread-2正在卖95张票
Thread-1正在卖94张票
Thread-0正在卖93张票
Thread-1正在卖92张票
结果分析:由于使用了ReentrantLock锁,所以共享变量ticket不会出现线程安全问题。至于这个锁的原理,我还没学到。学了再补充吧!