java多线程系列3-线程同步

如果一个资源被多个线程同时访问,可能会遭到破坏,这篇文章介绍java线程同步来解决这类问题

引入问题

某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

方法一:继承Thread类

public class SellTicket extends Thread {
 
    // 定义100张票
    // private int tickets = 100;
    // 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
    private static int tickets = 100;
 
    @Override
    public void run() {
        // 定义100张票
        // 是为了模拟一直有票
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}
/*
 * 继承Thread类来实现。
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建三个线程对象
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();
        SellTicket st3 = new SellTicket();
 
        // 给线程对象起名字
        st1.setName("窗口1");
        st2.setName("窗口2");
        st3.setName("窗口3");
 
        // 启动线程
        st1.start();
        st2.start();
        st3.start();
    }
}

方法二:实现Runnable接口

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;
 
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
            }
        }
    }
}
/*
 * 实现Runnable接口的方式实现
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();
 
        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
 
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

电影院售票程序,从表面上看不出什么问题,在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟

改实现接口方式的卖票程序,每次卖票延迟100毫秒,代码如下:

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;
    @Override
    public void run() {
        while (true) {
            // t1,t2,t3三个线程
            // 这一次的tickets = 1;
            if (tickets > 0) {
                // 为了模拟更真实的场景,我们稍作休息
                try {
                    Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
                //窗口1正在出售第1张票,tickets=0
                //窗口2正在出售第0张票,tickets=-1
                //窗口3正在出售第-1张票,tickets=-2
            }
        }
    }
}
/*
 * 实现Runnable接口的方式实现
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();
 
        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
 
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

出现问题:

相同的票出现多次

CPU的一次操作必须是原子性的

还出现了负数的票

随机性和延迟导致的

解决线程安全问题的基本思想与方法

首先想为什么出现问题?(也是我们判断是否有问题的标准)

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

如何解决多线程安全问题呢?

基本思想:让程序没有安全问题的环境。

把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

解决线程安全问题实现1--同步代码块

格式:synchronized(对象){需要同步的代码;}

同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

修改上面的代码如下:

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;
    //创建锁对象
    private Object obj = new Object();
    
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
}
/*
 * 同步代码块:
 *         synchronized(对象){
 *             需要同步的代码;
 *         }
 * 
 *         A:对象是什么呢?
 *             我们可以随便创建一个对象试试。
 *         B:需要同步的代码是哪些呢?
 *             把多条语句操作共享数据的代码的部分给包起来
 * 
 *         注意:
 *             同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
 *             多个线程必须是同一把锁。
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();
 
        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
 
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

注意:同步代码块可以用任意对象做锁

解决线程安全问题实现2--同步方法

就是把同步关键字加到方法上

1、同步方法的锁对象:this

public class SellTicket implements Runnable {
    private static int tickets = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票 ");
                }
            }
        }
    }
    private synchronized void sellTicket() {
        if(tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

2、静态方法的锁对象:类的字节码文件对象。

public class SellTicket implements Runnable {
    private static int tickets = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (SellTicket.class) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票 ");
                }
            }
        }
    }
    private static synchronized void sellTicket() {
        if(tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

同步的前提:

  • 多个线程
  • 多个线程使用的是同一个锁对象

同步的好处:同步的出现解决了多线程的安全问题。

同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

解决线程安全问题实现3--Lock锁的使用

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

ReentrantLock (Java Platform SE 6)

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

你可能感兴趣的:(java多线程系列3-线程同步)