死磕java底層(二)—訊息服務

喝水會長肉發表於2021-12-05

這一節作為上一節多執行緒的延續,先說一下java原生的阻塞佇列(Blocking Queue),之後再說一下JMS(Java Messaging Service,java訊息服務)以及它的實現之一ActiveMQ訊息佇列,所以都歸併到訊息服務中討論。

1.阻塞佇列(Blocking Queue)

BlockingQueue也是java.util.concurrent下的介面,它解決了多執行緒中如何高效傳輸資料的問題,通過這些高效並且執行緒安全的類,我們可以搭建高質量的多執行緒程式。 主要用來控制執行緒同步的工具。 BlockingQueue是一個介面,裡面的方法如下:

public interface BlockingQueue<E> extends Queue<E> {
    boolean add(E e);
    boolean offer(E e);
    void put(E e) throws InterruptedException;
    boolean offer(E e, long timeout, TimeUnit unit);
    E take() throws InterruptedException;
    E poll(long timeout, TimeUnit unit)
    int remainingCapacity();
    boolean remove(Object o);
    public boolean contains(Object o);
    int drainTo(Collection<? super E> c);
    int drainTo(Collection<? super E> c, int maxElements);
}
複製程式碼
  • 插入:
  1. add(anObject):把anObject加到BlockingQueue裡,即如果BlockingQueue可以容納,則返回true,否則丟擲異常,不好
  2. offer(anObject):表示如果可能的話,將anObject加到BlockingQueue裡,即如果BlockingQueue可以容納,則返回true,否則返回false.
  3. put(anObject):把anObject加到BlockingQueue裡,如果BlockQueue沒有空間,則呼叫此方法的執行緒被阻斷直到BlockingQueue裡面有空間再繼續, 有阻塞, 放不進去就等待
  • 讀取:
  1. poll(time):取走BlockingQueue裡排在首位的物件,若不能立即取出,則可以等time引數規定的時間,取不到時返回null; 取不到返回null
  2. take():取走BlockingQueue裡排在首位的物件,若BlockingQueue為空,阻斷進入等待狀態直到Blocking有新的物件被加入為止; 阻塞, 取不到就一直等
  • 其他
  1. int remainingCapacity();返回佇列剩餘的容量,在佇列插入和獲取的時候使用,資料可能不準。
  2. boolean remove(Object o); 從佇列移除元素,如果存在,即移除一個或者更多,佇列改變了返回true
  3. public boolean contains(Object o); 檢視佇列是否存在這個元素,存在返回true
  4. int drainTo(Collection<? super E> c); 移除此佇列中所有可用的元素,並將它們新增到給定 collection 中。(即取出放到集合中)
  5. int drainTo(Collection<? super E> c, int maxElements); 和上面方法的區別在於,指定了移動的數量; (取出指定個數放到集合) 主要的方法是:put、take一對阻塞存取;add、poll一對非阻塞存取。 上面說了BlockingQueue是一個介面,它有四個具體的實現類,常用的有兩個:
  6. ArrayBlockingQueue:一個由陣列支援的有界阻塞佇列,規定大小的BlockingQueue,其建構函式必須帶一個int引數來指明其大小.其所含的物件是以FIFO(先入先出)順序排序的。
  7. LinkedBlockingQueue:大小不定的BlockingQueue,其建構函式中可以指定容量,也可以不指定,不指定的話,預設最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在佇列滿的時候會阻塞直到有佇列成員被消費,take方法在佇列空的時候會阻塞,直到有佇列成員被放進來。 LinkedBlockingQueue和ArrayBlockingQueue區別: LinkedBlockingQueue和ArrayBlockingQueue比較起來,它們背後所用的資料結構不一樣,導致LinkedBlockingQueue的資料吞吐量要大於ArrayBlockingQueue,但線上程數量很大時其效能的可預見性低於ArrayBlockingQueue. 下面是是用BlockingQueue實現的生產者和消費者的示例: 生產者Product:
public class Product implements Runnable{

    BlockingQueue<String> queue;
    public Product(BlockingQueue<String> queue) {
        //建立物件時就傳入一個阻塞佇列
        this.queue = queue;
    }
    @Override
    public void run(){
        try {
            System.out.println(Thread.currentThread().getName()+"開始生產");
            String temp =  Thread.currentThread().getName()+":生產執行緒";
            queue.put(temp);//向佇列中放資料,如果佇列是滿的話,會阻塞當前執行緒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
複製程式碼

消費者Consumer:

public class Consumer implements Runnable{
    BlockingQueue<String> queue;
    public Consumer(BlockingQueue<String> queue) {
        //使用有參建構函式的目的是我在建立這個消費者物件的時候就可以傳進來一個佇列
        this.queue = queue;
    }
    @Override
    public void run() {
        Random random = new Random();
        try {
            while(true){
                Thread.sleep(random.nextInt(10));
                System.out.println(Thread.currentThread().getName()+ "準備消費...");
                String temp = queue.take();//從佇列中取任務消費,如果佇列為空,會阻塞當前執行緒
                System.out.println(Thread.currentThread().getName() + " 獲取到工作任務==== " +temp);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

測試類TestQueue:

public class TestQueue {
    public static void main(String[] args) {
        //新建一個阻塞佇列,佇列長度是5
        BlockingQueue<String> queue = new LinkedBlockingDeque<String>(5);
        //BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
        Consumer consumer = new Consumer(queue);
        Product product = new Product(queue);

        for(int i = 0;i<3;i++){
            new Thread(product,"product"+i).start();
        }

        //for (int i = 0;i<5;i++){
            new Thread(consumer,"consumer").start();
        //}
    }
}
複製程式碼

整套程式碼的意思就是初始化一個訊息佇列,裡面放String型別,佇列長度是5,使用生產者執行緒來模擬三個使用者發出請求,把使用者的請求資料暫時放在BlockingQueue佇列裡面,隨後消費者執行緒不斷的從佇列裡面取任務進行業務邏輯處理,直到佇列裡面消費的什麼都不剩了。由此可以看出訊息佇列有兩大特點:解耦和削峰填谷。生產者和消費者毛關係沒有,生產者往佇列裡放資料,消費者從佇列裡取資料,它們都跟佇列建立關係,解耦;生產者如果併發量很高也只不過是把資料先放到佇列裡,消費者可以慢慢吃,實際中不會立馬拖垮服務端。 參考地址:http://blog.csdn.net/ghsau/article/details/8108292

2.Java訊息服務

2.1JMS簡介

JMS即Java訊息服務(Java Message Service)用於在兩個應用程式之間,或分散式系統中傳送訊息,進行非同步通訊。JMS是一種與廠商(或者說是平臺)無關的 API。類似於JDBC(Java Database Connectivity):這裡,JDBC 是可以用來訪問許多不同關聯式資料庫的 API,而 JMS 則提供同樣與廠商無關的訪問方法,以訪問訊息收發服務。 許多廠商都支援 JMS,包括 IBM 的 MQSeries、BEA的 Weblogic JMS service和 Progress 的 SonicMQ等等。 JMS 可以讓你通過訊息收發服務從一個 JMS 客戶機向另一個 JMS客戶機傳送訊息。 訊息是 JMS 中的一種型別物件,由兩部分組成:報頭和訊息主體。報頭由路由資訊以及有關該訊息的後設資料組成;訊息主體則攜帶著應用程式的資料或有效負載。根據有效負載的型別來劃分,可以將訊息分為幾種型別,它們分別攜帶:簡單文字(TextMessage)、可序列化的物件 (ObjectMessage)、屬性集合 (MapMessage)、位元組流 (BytesMessage)、原始值流 (StreamMessage),還有無有效負載的訊息 (Message)。

2.2JMS的組成

JMS由以下元素組成: JMS提供者provider:面向訊息中介軟體的,JMS規範的一個實現。提供者可以是Java平臺的JMS實現,也可以是非Java平臺的面向訊息中介軟體的介面卡。 JMS客戶:生產或消費基於訊息的Java應用程式或物件(即生產者和消費者都統稱JMS客戶)。 JMS生產者:建立併傳送訊息的JMS客戶。 JMS消費者:接收訊息的JMS客戶。 JMS訊息:可以在JMS客戶之間傳遞資料的物件 JMS佇列:一個容納被髮送的正在等待閱讀的訊息的區域。一個訊息如果被閱讀,它將被從佇列中移走。 JMS主題:一種支援傳送訊息給多個訂閱者的機制。

2.3Java訊息服務模型

  • 點對點模型 在點對點佇列模型下,一個生產者向一個特定的佇列釋出訊息,一個消費者從該佇列中讀取訊息。這裡,生產者知道消費者的佇列,並直接將訊息傳送到消費者的佇列。 這種模式有如下特點: 只有一個消費者將獲得訊息; 生產者不需要消費者在消費該訊息期間處於執行狀態,消費者也同樣不需要在生產者在傳送訊息時處於執行狀態; 每一個成功處理的訊息都由消費者者簽收。
  • 釋出者/訂閱者模型 釋出者/訂閱者模型支援向一個特定的訊息主題釋出訊息。在這種模型下,釋出者和訂閱者彼此不知道對方,類似於匿名公告板。 這種模式有如下特點: 多個消費者可以獲得訊息; 在釋出者和訂閱者之間存在時間依賴性。釋出者需要建立一個訂閱(subscription),以便消費者能夠訂閱。訂閱者必須保持持續的活動狀態以接收訊息,除非訂閱者建立了持久的訂閱。

2.4訊息佇列(ActiveMQ)

ActiveMQ是JMS規範的一種實現,下面說如何使用

  • 下載ActiveMQ 去官方網站下載:http://activemq.apache.org/
  • 執行ActiveMQ 解壓apache-activemq-5.5.1-bin.zip(類似於Tomcat,解壓即可用),我在網上搜的有的人修改了配置檔案activeMQ.xml中連線的地址和協議,我在測試時沒有修改也可以測試成功,如果你測試不成功可以修改如下:
<transportConnectors>
   <transportConnector name="openwire" uri="tcp://localhost:61616"/>
   <transportConnector name="ssl"     uri="ssl://localhost:61617"/>
   <transportConnector name="stomp"   uri="stomp://localhost:61613"/>
   <transportConnector uri="http://localhost:8081"/>
   <transportConnector uri="udp://localhost:61618"/>
</transportConnectors>
複製程式碼

測試程式碼如下: 生產者Product:

public class Product {

    private String username = ActiveMQConnectionFactory.DEFAULT_USER;
    private String password = ActiveMQConnectionFactory.DEFAULT_PASSWORD;
    private String url = ActiveMQConnectionFactory.DEFAULT_BROKER_URL;

    private Connection connection = null;
    private Session session = null;
    private String subject = "myQueue";
    private Destination destination = null;
    private MessageProducer producer = null;
    /**
     * @Description 初始化方法
     * @Author 劉俊重
     * @Date 2017/12/20
     */
    private void init() throws JMSException {
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(username,password,url);
        connection = connectionFactory.createConnection();
        session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        destination = session.createQueue(subject);
        producer = session.createProducer(destination);
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
    }

    public void productMessage(String message) throws JMSException {
        this.init();
        TextMessage textMessage = session.createTextMessage(message);
        connection.start();
        System.out.println("生產者準備傳送訊息:"+textMessage);
        producer.send(textMessage);
        System.out.println("生產者已傳送完畢訊息。。。");
    }

    public void close() throws JMSException {
        System.out.println("生產者開始關閉連線");
        if(null!=producer){
            producer.close();
        }
        if(null!=session){
            session.close();
        }
        if(null!=connection){
            connection.close();
        }
    }
}
複製程式碼

消費者Consumer:

public class Consumer implements MessageListener,ExceptionListener{
    private String name = ActiveMQConnectionFactory.DEFAULT_USER;
    private String password = ActiveMQConnectionFactory.DEFAULT_PASSWORD;
    private String url = ActiveMQConnectionFactory.DEFAULT_BROKER_URL;
    private ActiveMQConnectionFactory connectionFactory = null;
    private Connection connection = null;
    private Session session = null;
    private String subject = "myQueue";
    private Destination destination = null;
    private MessageConsumer consumer = null;

    public static Boolean isconnection=false;
    /**
     * @Description 連線ActiveMQ
     * @Author 劉俊重
     * @Date 2017/12/20
     */
    private void init() throws JMSException {
        connectionFactory = new ActiveMQConnectionFactory(name,password,url);
        connection = connectionFactory.createConnection();
        session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        destination = session.createQueue(subject);
        consumer = session.createConsumer(destination);
    }

    public void consumerMessage() throws JMSException {
        this.init();
        connection.start();

        //設定訊息監聽和異常監聽
        consumer.setMessageListener(this);
        connection.setExceptionListener(this);
        System.out.println("消費者開始監聽....");
        isconnection = true;
        //Message receive = consumer.receive();
    }

    public void close() throws JMSException {
        if(null!=consumer){
            consumer.close();
        }
        if(null!=session){
            session.close();
        }
        if(null!=connection){
            connection.close();
        }
    }
    /**
     * 異常處理函式
     */
    @Override
    public void onException(JMSException exception) {
        //發生異常關閉連線
        isconnection = false;
    }

    /**
     * 訊息處理函式
     */
    @Override
    public void onMessage(Message message) {
        try {
            if(message instanceof TextMessage){
                TextMessage textMsg = (TextMessage) message;
                String text = textMsg.getText();
                System.out.println("消費者接收到的訊息======="+text);
            }else {
                System.out.println("接收的訊息不符合");
            }
        } catch (JMSException e) {
            e.printStackTrace();
        }

    }
}

複製程式碼

注意:消費者需要實現MessageListener和ExceptionListener來監聽收到訊息和出錯時的處理。 生產者測試類TestProduct:

public class TestProduct {
    public static void main(String[] args) throws JMSException {
        for(int i=0;i<100;i++){
            Product product = new Product();
            product.productMessage("Hello World!"+i);
            product.close();
        }
    }
}
複製程式碼

TestProduct是用來模擬生成100條訊息,寫入到ActiveMQ佇列中。 消費者測試類TestConsumer:

public class TestConsumer implements Runnable {
    static Thread thread = null;
    public static void main(String[] args) throws InterruptedException {
        thread = new Thread(new TestConsumer());
        thread.start();
        while (true){
            //時刻監聽訊息佇列,如果執行緒死了,則新建一個執行緒
            boolean alive = thread.isAlive();
            System.out.println("當前執行緒狀態:"+alive);
            if(!alive){
                thread = new Thread(new TestConsumer());
                thread.start();
                System.out.println("執行緒重啟完成");
            }
            Thread.sleep(1000);
        }
    }
    @Override
    public void run() {
        try {
            Consumer consumer = new Consumer();
            consumer.consumerMessage();
            while (Consumer.isconnection) {
                //System.out.println(123);
            }
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

TestConsumer這裡用了多執行緒,保證時刻有個執行緒活著等著接收ActiveMQ的訊息佇列並呼叫消費者處理。 總結:我的理解是執行緒間通訊使用queue,如BlockingQueue,程式間通訊使用JMS,如ActiveMQ。 另附上一篇將58架構師沈劍老師寫的訊息佇列的文章,可以作為參考:http://dwz.cn/78yLxL 需要強調的是任何一項技術的引用都要為解決業務問題服務,而不能是單純的炫技。舉個例子,就拿訊息服務來說,比如使用者註冊某個網站,註冊完了之後我要呼叫郵件和簡訊服務給他發通知,我可能還要通過他填的資訊,給他推薦一下可能認識的使用者,那麼這裡核心業務是註冊,其它的發通知和推薦使用者就可以放在訊息佇列裡處理,先響應註冊資訊,隨後呼叫其它服務來處理髮通知和推薦使用者這兩個業務。但是網站前期可能使用者量比較少,不用訊息佇列就能滿足我的需求了,引用訊息佇列反而會增加專案的複雜性,所以新技術的使用一定是需要解決業務的問題,而不是單純的炫技。 參考文件: http://blog.csdn.net/fanzhigang0/article/details/43764121 http://blog.csdn.net/u010702229/article/details/18085263

附一下個人微信公眾號,歡迎跟我交流。

Java開發日記

相關文章