同步方法解决实现Runnable的线程安全问题
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
package com.xzc;
class Window3 implements Runnable {
public Window3() {
}
private int ticket;
public Window3(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
}
}
}
public class Test3 {
public static void main(String[] args) {
Window3 w = new Window3(100);
new Thread(w, "窗口一").start();
new Thread(w, "窗口二").start();
new Thread(w, "窗口三").start();
}
}
输出结果:
注意这里:private synchronized void show()
就是同步方法处理。
这里的锁没有显示出来,但是也是有锁,在show方法里,锁就是this。
同步方法解决继承Thread的线程安全问题
此时不能像之前一样,直接把show方法定义为synchronized的,因为锁的不唯一。如果非想用同步方法解决,就要在synchronized前面加一个static声明为静态方法。
package com.xzc;
class Window4 extends Thread {
public Window4() {
}
public Window4(String name) {
super(name);
}
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket--);
}
}
}
public class Test4 {
public static void main(String[] args) {
Window4 w1 = new Window4("窗口一");
Window4 w2 = new Window4("窗口二");
Window4 w3 = new Window4("窗口三");
w1.start();
w2.start();
w3.start();
}
}
输出结果:
此时,我们的锁是当前类,Window4.class
同步方法总结
同步方法仍然涉及到锁,只是不需要我们显示声明
非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身(即Class类对象)
线程安全的单例模式之懒汉式
package demo01;
public class BankTest {
}
//懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
if (instance == null){
instance = new Bank();
}
return instance;
}
}
这样是不线程安全的,如果我们有多个线程run方法调用getInstance方法,其中一个线程进来,首次instance肯定是null,然后刚进if语句,可能会被阻塞,即使不被阻塞,cpu也可能切换到线程2,另外一个线程进来,那么instance会先后两次赋值,显然是不对的,因为instance是静态Bank类型变量。instance相当于是共享数据了。下面用同步方式修改为线程安全的:
package demo01;
public class BankTest {
}
//懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
/*synchronized(Bank.class) {
if (instance == null) {
instance = new Bank();
}
return instance;
}*/
//方式二
if (instance == null){
synchronized (Bank.class){
if(instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
我们先看方式一,return如果在synchronized同步代码块里面,为什么效率会低,这是因为不管instance是不是null,都进入同步代码块,如果线程非常多,就会产生效率低下问题,假设线程一拿到锁,产生一个instance返回,后面的所有线程还要在同步代码块前面等着,事实上这是无意义的,因为我们已经有instance了,后面的线程拿着instance返回就好了。而方式二就修正了这个问题,我们在同步代码块面前先判断instance是不是空,并且把return instance
提出来,这样线程一二三可能在同步代码块中抢一下锁,卡一会,但是后面的所有线程进来,直接就判断instance不是null了,然后直接返回instance就好了。
这样我们就把单例模式的懒汉式修改为线程安全的了。
线程死锁
当我们有两个锁嵌套的时候,就比较容易出现死锁问题:
package demo01;
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(() -> {
synchronized (s2) {
s1.append("c");
s2.append("3");
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
输出结果
这段代码其实就有死锁隐患,这是线程顺次执行完了,我们用sleep阻塞一下:
package demo01;
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(() -> {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
当我们在嵌套的分解处加入sleep进行阻塞后,就出现了死锁问题,这段代码执行起来,就和死循环一样,不会终止,但是也不会报错,这是因为我们第一个线程执行,拿到s1这把锁,执行sleep阻塞,这时候第二个线程很大概率也被调度,拿到s2这把锁,然后执行sleep阻塞,这时候第一个线程手里有s1这把锁,醒过来后发现没有s2这把锁,没法进去下面的同步代码段,这是因为s2被第二个线程拿了,同理s2醒过来后也拿不到s1,就僵持住了,发生了死锁。
也就是发生了:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。
Lock
package demo01;
import java.util.concurrent.locks.ReentrantLock;
//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{
private int ticket = 100;
//定义reentrantlock对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
//调用lock方法,锁住
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}
else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
new Thread(w,"Window - 1").start();
new Thread(w,"Window - 2").start();
new Thread(w,"Window - 3").start();
}
}
输出结果:
可以看到我们使用Reentrantlock类可以实现同步的方式,但是需要我们加锁解锁,synchronized是自动执行,加锁解锁一般就如上使用try-finally执行,一般就是先定义Reentrantlock对象,然后try里面锁住,finally里面解锁。Reentrantlock构造器还可以有一个参数:boolean fair,如果为true,则实现公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
package demo01;
import java.util.concurrent.locks.ReentrantLock;
//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{
private int ticket = 100;
//定义reentrantlock对象
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
//调用lock方法,锁住
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}
else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
new Thread(w,"Window - 1").start();
new Thread(w,"Window - 2").start();
new Thread(w,"Window - 3").start();
}
}
这里我们用了公平锁,结果:
我们从这可以看到,如果三个线程一开始的等待时间确定了,后面一定是一样的,1进拿锁,3进,2进则3一定等的比2长,3拿锁,1执行完了后2一定比1等的长,2拿锁,后面同理,所以公平锁可以看做先进先出。
synchronized与Lock的异同
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
一般优先顺序(实际上都一样):
Lock->同步代码块->同步方法
练习
package demo01;
import java.util.concurrent.locks.ReentrantLock;
class User implements Runnable {
private int money;
public User() {
}
public User(int initMoney) {
money = initMoney;
}
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
lock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money += 1000;
System.out.println(Thread.currentThread().getName() + "存了1000元," + "现有资金:" + money);
} finally {
lock.unlock();
}
}
}
}
public class Test {
public static void main(String[] args) {
User u = new User(0);
new Thread(u, "Human - 1").start();
new Thread(u, "Human - 2").start();
}
}
输出结果:
这里注意我们用的lock方式,因为采用实现runnable接口的方式来多线程,所以我们的锁是同一个,如果我们采用继承thread类方式,我们要把lock设为static的,以保证锁的唯一。