线程安全问题
0:总结
优先使用顺序
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
1:为什么要使用同步?看下面这个例子:
/**
* 例子:创建三个窗口卖票,总票数为10张.使用实现Runnable接口的方式
*/
class WindowThread implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
WindowThread windowThread = new WindowThread();
Thread t1 = new Thread(windowThread);
Thread t2 = new Thread(windowThread);
Thread t3 = new Thread(windowThread);
t1.start();
t2.start();
t3.start();
}
}
1、线程安全问题存在的原因:
由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
2、如何解决线程安全问题
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
3、java如何实现线程安全:线程的同步机制
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码块(即为操作共享数据的代码)
}
- 1、共享数据:多个线程共同操作的同一个数据(变量)
- 2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁。
要求:多个线程必须要共用同一把锁。 - 3、在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
- 4:、在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
1:使用同步代码块解决实现Runnable接口的线程安全问题
class Window2 implements Runnable {
int ticket = 100;// 共享数据
public void run() {
while (true) {
synchronized (this) {
//this表示当前对象,本题中即为w
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "售票,票号为:" + ticket--);
}
}
}
}
}
public class TestWindow2 {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Animal{
}
2:使用同步代码块解决继承Thread类的方式的线程安全问题
二:使用同步代码块解决继承Thread类的方式的线程安全问题
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
* 说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){
//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将其定义为同步方法。
例如:
public synchronized void show (String name){
….
}
1:使用同步方法解决实现Runnable接口的线程安全问题
一:使用同步方法解决实现Runnable接口的线程安全问题
* 关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
class WindowThread3 implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true){
show();
}
}
//同步show方法,继承Thread类方法一样,只需同步方法即可,同时需要给方法加static关键字,确保不会创建多个对象
private synchronized void show(){
//同步监视器默认为this
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
WindowThread3 windowThread3 = new WindowThread3();
Thread t1 = new Thread(windowThread3);
Thread t2 = new Thread(windowThread3);
Thread t3 = new Thread(windowThread3);
t1.start();
t2.start();
t3.start();
}
}
2:使用同步方法处理继承Thread类的方式中的线程安全问题
class Window4 extends Thread{
private static int ticket = 10;
@Override
public void run() {
//买票操作
while (true) {
show();
}
}
//生命成静态synchronized方法
private static synchronized void show(){
//同步监视器:Window4.class
//private synchronized void show(){ //同步监视器不唯一。此种解决方式是错误的
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.start();
w2.start();
w3.start();
}
}
方式三:Lock锁 — JDK 5.0新增
- 从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 Reentrantlock,可以显式加锁、释放锁。
class A {
//1.实例化ReentrantLock对象
private final ReenTrantLock lock = new ReenTrantLook();
public void m (){
lock.lock//2.先加锁
try{
//保证线程同步的代码
}finally{
lock.unlock();//3.后解锁
}
}
}
//注意:如果同步代码块有异常,要将unlock()写入finally语句块中
代码示例:
class Window implements Runnable{
private int ticket = 10;
//1.实例化ReentrantLock,如果使用继承的方式则ReentrantLock对象必须为静态的的
private ReentrantLock lock = new ReentrantLock();//默认构造方法的参数为false
//private ReentrantLock lock = new ReentrantLock(true);参数为true表示公平锁,就是谁等的时间最长,谁就先获取锁
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
}finally {
//3.3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
1、synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- =使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
4、处理单例模式之懒汉式的线程安全问题
1: 使用同步机制将单例模式中的懒汉式改写为线程安全的
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;
}
}
5、死锁问题
定义
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
举例
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 {
//如果先执行该线程,此处将其阻塞,之后有可能执行另一个线程。
//则s1和s2同时被上锁,就会出现死锁的状况
Thread.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(new Runnable() {
@Override
public void run() {
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();
}
}
6、练习
* 银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
分析:
1.是否是多线程问题? 是,两个储户线程
2.是否有共享数据? 有,账户(或账户余额)
3.是否有线程安全问题?有
4.需要考虑如何解决线程安全问题?同步机制:有三种方式。
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}