线程同步

  线程同步用于协调相互依赖的线程的执行。如果一个共享资源被多个线程同时访问,可能会遭到破坏。假设创建并启动100个线程,每个线程都往同一个账户中添加一元钱。

package edu.uestc.avatar;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AccountWithoutSync {
    //临界资源
    private static Account account = new Account();
    
    private static class Account{
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        public void deposit(int amount) {
            //为了故意放大数据破坏的可能性,采用下列语句,其实可用 balance += amount;代替
            int newBalance = balance + amount;
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance = newBalance;
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     *
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++)
            executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated()) {}
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

运行结果:

 

  那么,究竟是什么导致了程序的错误?下面给出了一个可能的场景

   在步骤1中,任务1从账户中获取余额数目。在步骤2中,任务2从账户中获取同样数目的余额。在步骤3中,任务1向账户写入一个新余额。在步骤4中,任务2也向该账户写入一个新余额。

  这个场景的效果就是任务1什么都没做,因为在步骤4中,任务4覆盖了任务1的结果。很明显,问题是任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程中的一个普遍问题,称为竞争状态(race condition)。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的(thread-safe).

Synchronized关键字

  为避免竞争状态,应该防止多个线程同时进入程序的某一特定部分,程序中的这部分称为临界区(critical region)。上面程序中的临界区为整个deposit方法。可以使用关键字synchronized来同步方法,以便一次只有一个线程可以访问这个方法。

  一个同步方法在执行之前需要加锁。锁是一种资源排他机制。对于实例方法,要给调用该方法的对象加锁。对于静态方法,需要给这个类加锁。如果一个线程调用一个对象上的同步实例方法(静态方法),首先给该对象(类)加锁,然后执行该方法,最后释放锁。在释放锁之前,其它调用该方法的线程将被阻塞,直到释放锁。

package edu.uestc.avatar;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AccountWithSync {
    //临界资源
    private static Account account = new Account();
    
    private static class Account{
        private static Lock lock = new ReentrantLock();
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        /**
         * 
         */
        public void deposit(int amount) {
            lock.lock();//获取锁
            try {
                //可以使用balance += amount;
                int newBalance = balance + amount;
                Thread.sleep(5);
                balance = newBalance;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//释放锁
            }
            
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     *
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++)
            executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated()) {}
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

同步代码块

  同步代码块允许设置同步方法中的部分代码,而不必是整个方法。这大大增加了程序的并发能力

 

package edu.uestc.monster;

public class CustomThreadTest {
    public static void main(String[] args) {
        //创建10个线程,代表10个售票窗口
        for(var i = 0; i < 10; i++) {
            var thread = new CustomThread("窗口-" + (i + 1));
            thread.start();//启动线程
        }
    }

}
/**
 * 由于Thread类实现了Runnable接口,
 * 所以可以定义一个类扩展自Thread类,并且覆盖run方法。
 *
 */
class CustomThread extends Thread{
    private static int ticket = 100;//共享资源,存在线程安全问题
    public CustomThread() {}
    public CustomThread(String name) {
        super(name);//Thread(String):指定线程名称
    }
    @Override
    public void run() {
        try {
            while(true) {
                synchronized (CustomThread.class) {
                    if(ticket <= 0)
                        break;
                    //Thread.currentThread():获取当前运行的线程
                    System.out.println(Thread.currentThread().getName() + "正在售出编号为 " + ticket-- + " 的火车票");
                    Thread.sleep(10);//模拟销售一张火车票的时间为1秒钟
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

利用加锁同步

基于synchronized关键字的锁机制有以下问题:

  • 锁只有一种类型,而且对所有同步操作都是一样的作用
  • 锁只能在代码块或方法开始的地方获得,在结束的地方释放
  • 线程要么得到锁,要么阻塞,没有其他的可能性

Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:

  • 可以添加不同类型的锁,例如读取锁和写入锁
  • 可以在一个方法中加锁,在另一个方法中解锁
  • 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作

显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:

  • ReentrantLock - 比synchronized稍微灵活一些的重入锁
  • ReentrantReadWriteLock - 在读操作很多写操作很少时性能更好的一种重入锁
package edu.uestc.avatar;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AccountWithLock {
    //临界资源
    private static Account account = new Account();
    
    private static class Account{
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        /**
         * 临界区:整个deposit
         *     synchronized防止多个线程同时今日临界区——一次只有一个线程可以访问该方法
         * @param amount 金额
         * 
         */
        public synchronized void deposit(int amount) {
            //可以使用balance += amount;
            int newBalance = balance + amount;
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance = newBalance;
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     *
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++)
            executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated()) {}
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

注意:解锁的方法unlock的调用最好能够在finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。

线程间通信

   锁上的条件可用于协调线程间的通信。一个线程可以指定在某种条件下该做什么。条件是通过调用Lock对象的newCondition()方法而创建的对象。一旦创建了条件,就可以使用await()、signal()和signalAll()方法来实现线程之间的通信。

package edu.uestc.avatar;

import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程间相互协作——通过条件实现线程间的通信
 *     条件是通过调用Lock对象的newCondition()方法创建的对象
 *     await():让当前线程进入等待,直到条件发生
 *     signal():唤醒一个等待的线程
 *     signalAll():唤醒所有等待的线程
 *     
 * 启动两个任务:一个任务向账户中存钱,另一个任务向账户中提款。当取钱金额大于账户余额,提款线程必须等待。
 * 不管什么时候,只要向账户中存入一笔钱,存钱线程必须通知提款线程重新尝试
 * 
 * 使用一个有条件的锁 newDeposit 。一个条件对应一个锁。在等待和通知状态之前,线程必须先获取该条件的锁。   
 *
 */
public class ThreadCooperation {
    private static Account account = new Account();
    
    private static class Account{
        //创建锁
        private static Lock lock = new ReentrantLock();
        //创建条件
        private static Condition newDeposit = lock.newCondition();
        //余额
        private int balance = 0;
        
        public void withdraw(int amount) {
            lock.lock();
            try {
                while(balance < amount) {//当余额小于取款金额,必须等待存款任务条件的发生
                    System.out.println("等待存款任务的完成.....");
                    newDeposit.await();
                }
                balance -= amount;
                System.out.println("取款完成,取款金额:" + amount);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        
        public void deposit(int amount) {
            lock.lock();
            try {
                balance += amount;
                System.out.println("存入一笔金额:" + amount);
                newDeposit.signalAll();
            } finally {
                lock.unlock();
            }
        }

        /*
         * public int getBalance() { return balance; }
         */
    }
    
    /**
     * 存款任务
     *
     */
    private static class DepositTask implements Runnable{
        @Override
        public void run() {
            try {
                while(true) {
                    account.deposit((int)(Math.random() * 100) + 1);
                    Thread.sleep(5);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static class WithdrawTask implements Runnable{
        @Override
        public void run() {
            while(true) {
                account.withdraw((int)(Math.random() * 100) + 1);
            }
        }
    }
    
    public static void main(String[] args) {
        var executor = Executors.newFixedThreadPool(2);
        executor.execute(new WithdrawTask());
        executor.execute(new DepositTask());
        executor.shutdown();
        
    }
}

传统的线程通信方式

  锁和条件是JDK 5中的新内容。在jdk 5之前,线程通信是使用对象的内置监视器实现的。锁和条件比内置监视器更加强大和灵活,因此无须使用内置监视器。然而,如果使用jdk 5.0以前的遗留代码,可能会使用到内置监视器。

  监视器(monitor)是一个相互排斥且具备同步能力的对象。监视器中的同一个时间节点上,只能有一个线程执行一个方法。线程通过获取监视器上的锁进入监视器,并且通过释放锁退出监视器。任意对象都可以是一个监视器。一旦一个线程锁住对象,该对象就称为监视器。加锁是通过在方法上或者代码块上使用synchronized关键字来实现的。在执行同步方法或者代码块之前,线程必须先获取锁。如果条件不适合线程继续在监视器内执行,线程可能在监视器中等待。可以对监视器对象调用wait()方法来释放锁,这样其他的一些监视器中的线程就可以获取它,也就有可能改变监视器中的状态。当条件合适的时候,另一些线程可以调用notify()或notifyAll()方法来通知一个或所有的等待的线程重新获取锁并且恢复执行  

package edu.uestc.canary.concurrent;

import java.util.concurrent.Executors;


public class TraditionalCooperation {
    private static Account account = new Account();
    public static class Account{
        private int balance;
        
        public synchronized void withdraw(int amount) {
            try {
                while(amount > balance) {
                    System.out.println("余额不足,终止当前线程并且释放对象的锁");
                    wait(); //必须先获取锁(在同步代码块或者方法中进行调用,否则IllegalMonitorStateException)
                }
                balance -= amount;
                System.out.println("取款金额:" + amount + "当前余额:" + balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public synchronized void deposit(int amount) {
            balance += amount;
            System.out.println("存款金额:" + amount + "当前余额:" + balance);
            notify();//通知一个等待的线程重新获取锁并恢复执行
        }
        
        /**
         * 取款任务
         *
         */
        private static class WithdrawTask implements Runnable{

            @Override
            public void run() {
                while(true) {
                    int amount = (int)(Math.random() * 10000 + 1);
                    account.withdraw(amount);
                }
            }
        }
        
        private static class DepositTask implements Runnable{

            @Override
            public void run() {
                try {
                    while(true) {
                        int amount = (int)(Math.random() * 10000 + 1);
                        account.deposit(amount);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public static void main(String[] args) {
            var executor = Executors.newFixedThreadPool(2);
            executor.execute(new DepositTask());
            executor.execute(new WithdrawTask());
            
            executor.shutdown();
        }
    }
}

 

生产者/消费者(另一篇)

CustomThread.class
posted @ 2022-05-29 09:32  Tiger-Adan  阅读(676)  评论(0编辑  收藏  举报