线程安全详解笔记

相关概念

线程安不安全: 多线程程序中,多个线程共同访问共享资源 ( 共有的资源 ) 时,由于线程的调度机制是抢占式调度,可能会发生多个线程在执行时,同时操作共享资源,导致程序执行结果与预期不一致的现象。
线程安全:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

案例

我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个 (本场电影只能卖100张票)。 我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票) 需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
模拟票:


public class Ticket implements Runnable{
    private int ticket=50;
    /**
     * 卖票操作
     */
    @Override
    public void run() {
//每个窗口的卖票操作
        //窗口永久开启
        while (true) {
            if (ticket > 0) {//有票可以继续卖
                //出票操作
                //使用sleep模拟下出票过程耗费的实际
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket--+"号票");
            } else {
                System.out.println(Thread.currentThread().getName() + "票已卖完!");
                break;//售完关闭窗口
            }
        }
    }
}

测试类:

public class TicketDemo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个线程作为三个售票窗口
        Thread thread1 = new Thread(ticket, "售票员1号");
        Thread thread2 = new Thread(ticket, "售票员2号");
        Thread thread3 = new Thread(ticket, "售票员3号");
        //同时售票
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

结果:
在这里插入图片描述
在这里插入图片描述

结果的问题:
出现两个甚至三个售票员售同一张票的情况;
还有出先-1这样非法票的情况;
这样票数不同步的问题就是一种线程不安全问题,原因是三个售票员同时访问了Ticket中的全局变量ticket,并且执行了写的操作。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码 去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

同步机制

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
同步操作的实现方式有以下三种:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
格式:

synchronized(同步锁){
      需要同步操作的代码
       }

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,在任何时候,最多允许一个线程拥有同步锁,只有拿到该锁的线程才能访问该对象,进入代码块,其他的线程只能在外等着 (BLOCKED)。
锁对象的类型:
a.this对象
b.class对象,如ticket.class //给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁
c.任意一个对象// Object lock = new Object(); 用这个比较容易理解

this作锁的示例
传入this关键字指代的是当前对象即Ticket的实例,其他试图访问该对象的线程将被阻塞。

public class Ticket implements Runnable {
    private int ticket = 50;
    /**
     * 卖票操作
     */
    @Override
    public void run() {
//每个窗口的卖票操作
        //窗口永久开启
        while (true) {
            synchronized (this) {

                if (ticket > 0) {//有票可以继续卖
                    //出票操作
                    //使用sleep模拟下出票过程耗费的实际
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
                } else {
                    System.out.println(Thread.currentThread().getName() + "票已卖完!");
                    break;//售完关闭窗口
                }
            }

        }
    }
}

结果正常:
在这里插入图片描述
总结:给某个对象加锁:
1)当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。

public void method3(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj)
   {
      // todo
   }
}

2)当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

class Test implements Runnable
{
   Object lock = new Object();  // 随便的一个对象锁
   public void method()
   {
      synchronized(lock) {
         // todo 同步代码块
      }
   }
 
   public void run() {
 
   }
}

同步方法:

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
锁对象:
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

锁对象是谁调用这个方法就是谁,隐含锁对象就是this

示例:

public class TicketRunnable2 implements Runnable  {
    private Integer ticket = 50;
    @Override
    public void run() {
        //窗口永久开启
        while (true) {
          if (!sellTicket())
              break;
        }
    }
     /**
     * 锁对象是谁调用这个方法就是谁,隐含锁对象就是this 
     * @return
     */
    private synchronized boolean sellTicket() {
        if (ticket > 0) {//有票可以继续卖
            //出票操作
            //使用sleep模拟下出票过程耗费的实际
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
            return true;
        } else {
            System.out.println(Thread.currentThread().getName() + "票已卖完!");
            return false;
        }
    }
}

Lock锁:

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。
    使用:
public class LockTicket implements Runnable {
    private int ticket = 50;
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            if (ticket > 0) {
                //出票操作
                //使用sleep模拟下出票过程耗费的实际
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
            } else {
                System.out.println(Thread.currentThread().getName() + "票已卖完!");
                break;
            }
            lock.unlock();
        }
    }
}

线程的状态:

线程状态 导致状态发生条件

  1. NEW(新建) :线程刚被创建,但是并未启动。还没调用start方法。 1.
  2. Runnable(可 运行):线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。 对应于操作系统中的线程的就绪和运行状态。
  3. Blocked(锁阻 塞):当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
  4. Waiting(无限 等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
  5. Timed Waiting(计时等待) :同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。
  6. Teminated(被 终止) :因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
    线程生命周期的线程图:
    在这里插入图片描述
    注意:被唤醒的线程并不能立即执行,而是要重新去竞争锁,得到锁后才能继续执行。得不到锁就会处于BLOCKED阻塞状态。

上述的生命周期中,创建、执行、阻塞与结束都比较好理解,这里主要通过一个联系学习下Waitting
waiting的案例。


public class WaitingTestSyschronized {
    private static Object object = new Object();

    public static void main(String[] args) {

        //匿名内部类方式创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (object) {
                        try {
                            System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用wait方法,进入waiting状态,释放锁对象");
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "从waiting状态中醒来,获得锁对象继续执行!");
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) { //每三秒唤醒一次
                    synchronized (object) {
                        try {
//                    Thread.sleep(3000);wait和sleep的区别在下面有讲
                            object.wait(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用notify方法,释放锁对象");
                       object.notify();//唤醒
                    }
                }
            }
        }).start();
    }
}

结果:
在这里插入图片描述

下面用Lock锁示例了等待和唤醒的方式,lock锁的方式不能用wait和notify,否则会报IllegalMonitorStateException错误。
因为调用wait的对象必须是唯一的,必须被synchronized加锁。所以要用lock自己特定的方式来加锁解锁,以及实现等待唤醒。

ReentrantLock.lock可以对应理解成synchronized刚进入代码块获取到锁
ReentrantLock.unlock可以对应理解成synchronized代码块结束释放锁
Condition condition = reentrantLock.newCondition()
condition.await 可以理解成 Object.wait 方法的封装
condition.signal 可以理解成 Object.notify 方法的封装


public class WaitingTest {
//Lock锁
    private static Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public static void main(String[] args) {
        //匿名内部类实现线程的创建
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lock.lock();//加锁
                    try {
                        System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用await方法,进入waiting状态,释放锁对象");
                        condition.await();
//                        condition.await(5000,TimeUnit.MILLI_SCALE); //计时等待, 5秒时间到,自动醒来
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "从waiting状态中醒来,获得锁对象继续执行!");
                    lock.unlock();//解锁
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) { //每三秒唤醒一次
                lock.lock();
                try {
//                    Thread.sleep(3000);
                    condition.await(3000, TimeUnit.MILLI_SCALE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用signal方法,释放锁对象");
                condition.signal();//唤醒
                lock.unlock();//解锁
            }
            }
        }).start();
    }
}

结果:
在这里插入图片描述

wait和sleep的区别

1、sleep是线程中的方法,但是wait是Object中的方法。

2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字,只有synchronized加锁的对象才能调用wait和notify。

4、sleep不需要被唤醒(休眠之后推出阻塞),但是没有指定时间的wait需要被其他线程唤醒,指定时间自动唤醒的则不需要。

线程间的通信

为什么需要线程间的通信:多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
实现线程通信的方式:多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就 是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效 的利用资源。而这种手段即—— 等待唤醒机制

上述的wait与notify,以及await和signal就是一种等待唤醒机制,类似操作系统了的pv操作。

注意: 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。

使用线程的时候就去创建一个线程,这样实现起来非常简便,但并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束的情况下,频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。
就需要使用线程池来实现线程的复用,提高效率。有关线程池可以看这篇文章。
线程池使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值