JAVA三个线程依次打印ABC

时间:2022-09-28 18:31:11

一、一些简单概述

       多线程情形下对共享资源的访问是需要互斥的,比如对一个变量的读写,一个线程读写这个变量的时候,其它线程就不能对这个变量进行读写。Java提供了synchronized关键字来支持互斥,它既可以修饰需要访问共享资源的方法(称为同步方法),也可以直接包裹访问共享资源的代码块(称为同步块)。两种书写形式分别如下:

//同步方法
public synchronized void foo(){/*....*/}

//同步块
synchronized(lockObj){//lockObj称为对象锁

}

        此外共享资源必须设置为private类型,否则其它线程便可以直接访问共享资源,而不是通过synchronized的方法或代码片段来访问,就破坏了互斥条件。这一点很好理解,就不举例子了。

        当一个线程调用同步方法或者执行同步块时,其它任何线程的同步方法和同步块都将被阻塞(前提是使用的是同一个对象锁)。为什么呢?其实每个对象都有一个内置的锁,使用synchronized修饰方法时,就会对该类的实例对象进行上锁,这样同一个实例的其它同步方法都将被阻塞。synchronized用于同步块时,需要显示的提供一个用于加锁的对象lockObj,这样其它使用同一个lockObj的同步块都将被阻塞。

        线程之间除了互斥,有时候还需要协作,例如著名的生产者-消费者问题。synchronized不仅可以做到线程互斥,也可以做到线程协作,只是还需要wait()、notify()、notifyAll()三个方法。这三个方法用于线程的通信,都是lockObj的方法,其中wait()允许线程放弃锁,将线程阻塞并等待唤醒(直到有线程调用notify()或者notifyAll())。notiyf()用于唤醒等待的线程,调用后jvm会在该方法或者代码块完成后,从等待该锁线程中随机选取一个线程进行唤醒,唤醒后的线程可以从wait()后面开始继续运行。notifyAll()顾名思义就是唤醒所有等待该锁的线程。根据《Effective Java》一书的说法,一般情况下建议使用notifyAll(),并将wait()置于循环中。原因是所有等待该对象锁的线程(数量应该>2)唤醒条件可能是不一样的,仅仅用notify(),那么唤醒的线程可能并不符合唤醒的条件。将wait()置于循环中,就可以让被错误唤醒的线程继续判断条件,而不是直接执行wait()后面的代码。

二、实例

        先从经典的生产者消费者问题入手。题目很简单:一个生产者线程Producer,一个消费者线程Comsumer,一个产品队列Queue,其中生产者生产产品并放入产品队列,消费者从产品队列中取出产品消费,产品队列是共享资源,需要互斥访问,而且产品队列满的时候,生产者线程需要等待消费者取出产品,产品队列空的时候,消费者线程需要等待生产者放入产品。代码如下:

package main;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;


public class test {
public static void main(String args[]) {
Queue<Integer> buffer = new LinkedList<Integer>(); //产品队列
int maxSize = 10; //产品队列最大容量
Thread producer = new Producer(buffer, maxSize, "PRODUCER");
Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");
producer.start();
consumer.start();
}
}


class Producer extends Thread
{
private Queue<Integer> queue;
private int maxSize;
public Producer(Queue<Integer> queue, int maxSize, String name){
super(name);
this.queue = queue;
this.maxSize = maxSize;
}
@Override public void run()
{
while (true)
{
synchronized (queue) {
while (queue.size() == maxSize) { //先在循环中判断条件,避免错误唤醒
try {
System.out .println("队列满,等待消费者取出产品。");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace(); }
}
Random random = new Random();
int i = random.nextInt();
System.out.println("产品编号 : " + i);
queue.add(i);
queue.notifyAll(); //唤醒消费者线程取产品
}
}
}
}

class Consumer extends Thread {
private Queue<Integer> queue;
private int maxSize;
public Consumer(Queue<Integer> queue, int maxSize, String name){
super(name);
this.queue = queue;
this.maxSize = maxSize;
}
@Override public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) { //先在循环中判断条件,避免错误唤醒
System.out.println("产品队列空,等待生产者生产产品。");
try {
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("消费的产品编号 : " + queue.remove());
queue.notifyAll(); //唤醒生产者线程生产产品
}
}
}
}

        回到打印ABC的主题,在讲三个线程顺序打印ABC之前,先讲两个线程打印AB,线程A打印A,线程B打印B,并且顺序是ABABAB...。很明显A、B线程需要协作,A打印完毕后需要等待B打印完毕才能继续打印,同样B打印完毕后需要等待A打印完毕才能继续打印。代码如下:

package main;
class Print{
private static boolean flag = false;//判断条件
private static Object lockObj = new Object();//这里使用的是static,保证线程使用的是同一个对象锁。也可以仿照上面的例子中buffer,将对象锁通过构造函数传进来。
public void printA(){
int count = 10;
while((count --) >0){
synchronized(lockObj){
try {
while(flag == true){//在循环中判断条件,避免错误唤醒。
lockObj.wait();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.print('A');
flag = true;//改变条件并唤醒等待线程
lockObj.notifyAll();
}
}
}
public void printB(){
int count =10;
while((count --) >0){
synchronized(lockObj){
try {
while(flag == false){
lockObj.wait();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.print('B');
flag = false;
lockObj.notifyAll();
}
}
}
}
class PrintA implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
new Print().printA();
}
}
class PrintB implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
new Print().printB();
}
}
public class test {
public static void main(String[] args) {
// TODO Auto-generated method stub
new Thread(new PrintA()).start();
new Thread(new PrintB()).start();
}
}

        实现了两个线程顺序打印AB,现在来看三个线程顺序打印ABC。两个线程,我们使用了一个对象锁,一个条件变量。三个线程可以增加一个布尔条件变量就可以了,不过这里条件变量我们用int表示,其中1表示该打印A,2表示该打印B,3表示该打印C,代码如下:

package main;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Print{
private static int flag = 1;//条件变量
private static Object synObj = new Object();//对象锁

public void printA() throws InterruptedException{
for(int i =0;i<10;i++){
synchronized(synObj){
while(flag != 1){
synObj.wait();
}
System.out.print('A');
flag = 2;//改变条件并唤醒等待线程
synObj.notifyAll();
}
}
}
public void printB() throws InterruptedException{
for(int i =0;i<10;i++){
synchronized(synObj){
while(flag != 2){
synObj.wait();
}
System.out.print('B');
flag = 3;
synObj.notifyAll();
}
}
}
public void printC() throws InterruptedException{
for(int i =0;i<10;i++){
synchronized(synObj){
while(flag != 3){
synObj.wait();
}
System.out.print('C');
flag = 1;
synObj.notifyAll();
}
}
}
}
class ThreadA implements Runnable {
@Override
public void run() {
try {
new Print().printA();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class ThreadB implements Runnable {
@Override
public void run() {
try {
new Print().printB();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class ThreadC implements Runnable {
@Override
public void run() {
try {
new Print().printC();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class test {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
new Thread(new ThreadC()).start();
}
}
另外,网上有使用AutomaticInteger来实现的,可以参考下面的文章:基于AutomaticInteger实现三个线程打印ABC