多线程的安全问题 | 8月更文挑战

174 阅读5分钟

1. 线程安全问题

当多个线程并发执行的时候,如果操作同一个数据(共享数据),就会造成共享数据的不准确和不合理。这种现象就是线程不安全,即产生了线程安全问题。 线程安全问题的解决方案: 使用同步机制来解决线程安全问题,线程同步是指在某一时间内只允许一个线程在执行并访问共享数据,又叫互斥。 线程同步的2种方式:

同步代码块 2.同步方法 同步代码块语法: synchronized (同步监视器){ //代码块 } 同步监视器的使用要点: .同步监视器必须是Object类型,不能是基本数据类型 .任何Object对象都可以作为同步监视器(锁),但是多个线程必须共用同一个锁(对象) 如何使用同步代码块? 同步代码块说白是就是使用一个锁对象把某个代码块包起来,把代码块锁住,在某个时刻内只能有1个线程抢到这个锁,就可以进到代码块中执行,其中的代码一定是对共享数据的操作. 同步方法语法: A. 非静态的同步方法 public synchronized 返回类型 方法名(){ //对共享资源进行操作的代码 } B. 静态的同步方法 public static synchronized 返回类型 方法名(){ //对共享资源进行操作的代码

}

推荐使用同步代码块

线程通信

wait() :等待,让正在执行的线程释放CPU执行权,进入阻塞状态,其它线程有机会执行。 notify():唤醒,唤醒正在排队等待的线程,一次唤醒一个,而且是任意的。 notifyAll():唤醒正在排队等待的所有 线程。 注意:1).以上三个方法必须是在线程获取到锁时,才能调用,即只能在同步方法或代码块中调用。 如果不在同步方法或代码块中调用以上方法,报非法监视器状态异常: java.lang.IllegalMonitorStateException . 以上3个方法的调用者只能是对象锁,例如:

@Override
public void run() {
    while (true) {
      synchronized (obj) {
           //唤醒
           obj.notify();
          if (i <= 100) {
              System.out.println(Thread.currentThread().getName() + "," + i);
              i++;
              try {
                  //等待
                  obj.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              break;
          }
      }
    }
} 
2. 解决线程安全问题的方法2-使用Lock
lock和synchronized的区别:synchronized对锁的操作是隐式的,而Lock是显式的,即用户必须手动的去加锁和释放锁.
class TicketRunnable implements Runnable {    
// 共享数据,保存火车票的总量   
 private  int ticketNum = 100;    
 //创建锁对象    
 Lock  lock=new ReentrantLock();    
 @Override    public void run() {       
 while (true) {           
  try {               
  //加锁,相当于synchronized(this)                
  lock.lock();                // 当票数小于等于0时,窗口停止售票,跳出死循环              
    if (ticketNum <= 0)                  
      break;                
      // 当票数大于0时,售票窗口开始售票               
       //Thread.sleep(10); 
       // 模拟切换线程的操作                
      // 输出售票窗口(线程名)卖出的哪一张票              
        String name = Thread.currentThread().getName(); 
        System.out.println(name + "--卖出第" + ticketNum + "张票");              
          // 卖票之后,总票数递减               
           ticketNum--;           
            } catch (Exception e) {               
             e.printStackTrace();          
               }finally {                
               //解锁                
               lock.unlock();            
               }        
            }  
       }
  }

线程的生命周期会经历以下几个状态: 新建:new创建线程对象时

就绪:调用start()方法时 运行:调用run()方法时 阻塞: 多种原因可导致阻塞 死亡:多种原因 新建、就绪状态 使用new 关键字创建一个线程时,该线程处于新建状态 线程对象调用 start()方法时,该线程处于就绪状态(线程获得CPU,等待执行) 线程的启动是从调用start()方法开始的,而不是run()方法 永远不要调用线程对象的run()方法 如果直接调用 run() 方法,系统会把该线程对象当成普通对象,run()方法也将变成一个普通方法(而不是线程执行体)而立即执行。 如果直接调用了 run() 方法,则该线程不再处于新建状态,不能再次调用 start()方法,否则会报IllegalThreadStateException异常 如果直接调用了 run() 方法,则在run() 方法里不能直接通过this.getName() 方法获得线程名(此时获取的是对象名,因为此时已经没有线程体了,线程对象变成了普通对象),而是通过Thread.currentThread().getName() 获得。 如果希望线程对象调用start() 方法后立即执行run() 方法(线程体),可通过Thread.sleep(1)让主线程睡眠1毫秒,而给其他线程执行机会。 运行、阻塞状态 如果CPU是单核,则在任一时刻都只有一个线程在执行。当线程数据大于核数时,就会出现线程轮换。

所有桌面和服务器系统都采用的是抢占式调度策略,即当前线程在系统允许的执行时间之后,就给其他线程获得执行机会,且优先给优化级高的线程。

有些小型系统如手机会采用协作式调度策略,即只能当前线程主动放弃所占资源(调用sleep()或yield()方法)

线程从阻塞状态解除后只能转变为就绪状态,而就绪状态变为运行状态只能由系统线程调度转换。

线程调用了yield()方法也会重新进入就绪状态

发生以下情况时,线程将进入阻塞状态: 调用sleep()方法时。此时会放弃它所占用的处理器资源。【过了sleep指定时间不再阻塞】 调用一个阻塞式IO方法还没有返回之前,该线程被阻塞。【阻塞IO方法返回后不再阻塞】 试图获取一个正被其他线程所持有的同步监视器。【拿到监听器不再阻塞】 等待通知时(notify)。【其他线程调用了notify时不再阻塞】 调用suspend()方法将程序挂起时。【线程调用resume()方法时撤销挂起时不再阻塞】 线程死亡 线程死亡情况: 线程正常结束(run或call方法执行完)

线程抛出一个未捕获的Exception或Error

线程自己调用stop()方法(该方法容易导致死锁)

一旦子线程启动后,它就拥有和主线程相同的地位,不受主线程的影响。

线程对象只能调用一次start()方法,且只能在新建状态时才能调用。否则会抛出IllegalThreadStateException异常。