当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
1 同一个资源问题
1、局部变量不能共享
public class SaleTicketDemo1 {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
public void run(){
int total = 100;
while(total>0) {
System.out.println(getName() + "卖出一张票,剩余:" + --total);
}
}
}
2、不同对象的实例变量不共享
public class SaleTicketDemo2 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.start();
t2.start();
t3.start();
}
}
class TicketSaleThread extends Thread{
private int total = 10;
public void run(){
while(total>0) {
System.out.println(getName() + "卖出一张票,剩余:" + --total);
}
}
}
3、静态变量是共享的共享
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread();
TicketThread t3 = new TicketThread();
t1.start();
t2.start();
t3.start();
}
}
class TicketThread extends Thread{
private static int total = 10;
public void run(){
while(total>0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,剩余:" + --total);
}
}
}
共享,但是有线程安全问题
4 同一个对象的实例变量共享
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr,"窗口一");
Thread t2 = new Thread(tr,"窗口一");
Thread t3 = new Thread(tr,"窗口一");
t1.start();
t2.start();
t3.start();
}
}
class TicketSaleRunnable implements Runnable{
private int total = 10;
public void run(){
while(total>0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
}
}
}
但是存在0票和-1票情况
2 尝试解决线程安全问题
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。 格式:
3 锁对象选择
同步锁对象:
-
锁对象可以是任意类型。
-
多个线程对象 要使用同一把锁。
1 同步方法的锁对象问题
(1)静态方法:当前类的Class对象
(2)非静态方法:this
public class SaleTicketSafeDemo1 {
public static void main(String[] args) {
// 2、创建资源对象
Ticket ticket = new Ticket();
// 3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {
while (true) {
try {
Thread.sleep(10);// 加入这个,使得问题暴露的更明显
ticket.sale();
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
try {
Thread.sleep(10);// 加入这个,使得问题暴露的更明显
ticket.sale();
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
while (true) {
try {
Thread.sleep(10);// 加入这个,使得问题暴露的更明显
ticket.sale();
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
// 1、编写资源类
class Ticket {
private int total = 10;
//非静态方法隐含的锁对象就是this
public synchronized void sale() {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
} else {
throw new RuntimeException(Thread.currentThread().getName() + "发现没有票了");
}
}
public int getTotal() {
return total;
}
}
2、同步代码块的锁对象
同步锁对象:
-
锁对象可以是任意类型。
-
多个线程对象 要使用同一把锁。
-
习惯上先考虑this,但是要注意是否同一个this
public class SaleTicketSafeDemo1 { public static void main(String[] args) { // 2、创建资源对象 Ticket ticket = new Ticket(); // 3、启动多个线程操作资源类的对象 Thread t1 = new Thread("窗口一") { public void run() { while (true) { try { Thread.sleep(10);// 加入这个,使得问题暴露的更明显 ticket.sale(); } catch (Exception e) { e.printStackTrace(); break; } } } }; Thread t2 = new Thread("窗口二") { public void run() { while (true) { try { Thread.sleep(10);// 加入这个,使得问题暴露的更明显 ticket.sale(); } catch (Exception e) { e.printStackTrace(); break; } } } }; Thread t3 = new Thread(new Runnable() { public void run() { while (true) { try { Thread.sleep(10);// 加入这个,使得问题暴露的更明显 ticket.sale(); } catch (Exception e) { e.printStackTrace(); break; } } } }, "窗口三"); t1.start(); t2.start(); t3.start(); } } // 1、编写资源类 class Ticket { private int total = 10; public void sale() { synchronized (this) { if (total > 0) { System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total); } else { throw new RuntimeException(Thread.currentThread().getName() + "发现没有票了"); } } } public int getTotal() { return total; } }
4 锁的范围问题
锁的范围太小:不能解决安全问题
锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。
-
锁范围太小示例
public class SaleTicketSafeDemo2 { public static void main(String[] args) { TicketRunnable tr = new TicketRunnable(); Thread t1 = new Thread(tr,"窗口一"); Thread t2 = new Thread(tr,"窗口二"); Thread t3 = new Thread(tr,"窗口三"); t1.start(); t2.start(); t3.start(); } } class TicketRunnable implements Runnable { private int ticket = 10; @Override public void run() { while(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { //if (ticket > 0) {//条件没有锁进去 System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--); //} } } } }
锁范围太大示例
public class SaleTicketSafeDemo3 { public static void main(String[] args) { TicketRunnableDemo tr = new TicketRunnableDemo(); Thread t1 = new Thread(tr,"窗口一"); Thread t2 = new Thread(tr,"窗口二"); Thread t3 = new Thread(tr,"窗口三"); t1.start(); t2.start(); t3.start(); } } class TicketRunnableDemo implements Runnable { private int ticket = 10; @Override public synchronized void run() { while(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--); } } }