文章目录
1.经典线程同步例子——卖票
理想状态下卖票
极端状态下卖票
使用实现Runnable接口的方式卖票
例子:使用实现Runnable接口的方式,创建三个窗口卖票,总票数为100张。
1.问题:卖票过程中,出现了重票,错票–>出现了线程的安全问题。
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来.直到线程a操作完ticket时, 其他线程才可以开始操作ticket.这种情况即使线程a出现了阻塞,也不能改变。
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1.操作共享数据的代码,即为需要被同步的代码.
2.共享数据:多个线程共同操作的变量.比如:ticket就是共享数据.
3.同步监视器,俗称:锁. 任何一个类的对象,都可以充当锁.
要求:多个线程必须要共用同一把锁.
补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器.
/**
* 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
*
* 1.问题:卖票过程中,出现了重票,错票-->出现了线程的安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票.
* 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来.直到线程a操作完ticket时,
* 其他线程才可以开始操作ticket.这种情况即使线程a出现了阻塞,也不能改变.
* 4.在Java中,我们通过同步机制,来解决线程的安全问题.
* 方式一:同步代码块
*
* synchronized(同步监视器){
* //需要被同步的代码
* }
* 说明:1.操作共享数据的代码,即为需要被同步的代码
* 2.共享数据:多个线程共同操作的变量.比如:ticket就是共享数据.
* 3.同步监视器,俗称:锁. 任何一个类的对象,都可以充当锁.
* 要求:多个线程必须要共用同一把锁.
*
* 补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器.
* 方式二:同步方法
*
*
* 5.同步代码块的方式,解决了线程安全的问题.---好处
* 操作同步代码时,只能有一个线程参与,其他线程等待.相当于一个单线程的过程,效率低.---局限性.
*/
class Window implements Runnable{
private int ticket=100;
//Object obj=new Object();
@Override
public void run() {
while(true){
synchronized(this){ //同步此时的this:唯一的Window对象
if (ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class TestRunnable{
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.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式二:同步方法
使用同步方法来解决实现Runnable接口的线程安全问题。
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明.
2.非静态的同步方法,同步监视器是:this
3.静态的同步方法,同步监视器是:当前类本身
/**
* 使用同步方法来解决实现Runnable接口的线程安全问题
* 1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明.
* 2.非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
*/
class Window implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){ //同步监视器:this
if (ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
ticket--;
}
}
}
public class TestRunnable{
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.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
使用继承Thread类的方式卖票
例子:使用继承Thread类的方式,创建三个窗口卖票,总票数为100张。
方式一:同步代码块
/**
* 使用同步代码块解决继承Thread类的方式的线程安全问题。
* 例子:创建三个窗口卖票,总票数为100张,使用实继承Thread类的方式
*
*说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
*/
class Windows2 extends Thread{
private static int ticket=100;
private static Object obj=new Object(); //静态锁,共用一把锁
@Override
public void run() {
while (true){
//正确的
//synchronized (obj){
synchronized (Windows2.class){ //类是对象,类唯一。类锁
if (ticket>0){
try{
Thread.sleep(100);
System.out.println(getName()+":卖票,票号为:"+ticket);
ticket--;
}catch (InterruptedException e){
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class TestThread {
public static void main(String[] args) {
Windows2 t1=new Windows2();
Windows2 t2=new Windows2();
Windows2 t3=new Windows2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式二:同步方法
/**
* 使用同步方法解决继承Thread类的方式的线程安全问题。
*/
class Windows2 extends Thread{
private static int ticket=100;
private static Object obj=new Object(); //静态,共用一把锁
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){ //同步监视器:Windows2.calss
if (ticket > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestThread {
public static void main(String[] args) {
Windows2 t1=new Windows2();
Windows2 t2=new Windows2();
Windows2 t3=new Windows2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.线程安全的单例模式之懒汉式
/*
*使用同步机制将单例模式的懒汉式改为线程安全的.
*
*/
public class SingleTest {
}
class Single{
private Single(){}
private static Single instance=null;
public static Single getInstance(){
//效率更高
if (instance==null){
synchronized(Single.class){
if (instance==null){
instance=new Single();
}
}
}
return instance;
}
}
3.线程的死锁问题
- 死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
- 解决办法
- 专门的算法、原则。
- 尽量减少同步资源的定义。
- 尽量避免嵌套同步。
- 死锁例子
public class SuoTest {
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 {
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();
}
}
4.解决线程安全问题的方式三:Lock锁
- Lock锁
- 从jdk5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,同拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
- Lock锁解决线程安全问题例子
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三:Lock锁 ---JDK5.0新增
*/
class Window implements Runnable{
private int ticket=100;
//1.实例化ReentrantLock
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class TestRunnable{
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.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- synchronized与Lock的异同?
相同: 二者都可以解决线程安全问题。
不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock需要手动的启动同步lock(),同时结束同步也需要手动的实现unlock()。
5.练习题
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
/**
* 题目:银行有一个账户。有两个储户分别向同一个账户存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();
}
}