Java之多线程编程

一、优缺点

1.1 优势

  • 资源利用率更好;
  • 程序设计在某些情况下更加简单;
  • 资源响应速度更快;

1.2 劣势

  • 开发设计更加复杂,涉及到多线程访问共享数据等问题;
  • 上下文切换的开销增加。当CPU从一个线程切换到另外一个线程时,需先存储当前线程的数据,计数器等,然后载入另外一个线程的本地数据等,称之为“上下文切换”;

二、Java创建多线程

2.1 继承Thread父类

  • 创建Thread子类的一个实例,并且重写run方法,run方法会调用start( )方法之后被执行。
public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println("线程正在运行....");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  • 此外,可以通过匿名类的形式创建一个Thread
Thread thread1 = new Thread() {
    public void run() {
        System.out.println("线程1正在运行.....");
    }
};
thread1.start();

2.2 实现Runnable接口

  • 新建一个实现了java.lang.Runnable接口的类实例,实例中方法可以被线程调用。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable正在运行");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}
  • 此外,可以创建一个实现Runnable接口的匿名类
Runnable runnable1 = new Runnable() {
    public void run() {
        System.out.println("Runnable1 is working");
    }
};
Thread thread1 = new Thread(runnable1);
thread1.start();

三、线程安全

  • 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如同一块内存区(变量,数组,对象)、系统(数据库,web服务等)、文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源还是安全的。
  • 当多个线程竞争同一个资源时,如果对资源的访问顺序敏感,就会存在竞态条件,导致竞态条件发生的代码区被称为临界区。
  • 如果一个资源的创建、使用、销毁都在同一个线程内完成,而且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

四、Java同步块

  • Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上,所有同步在一个对象上的同步块同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

同步块的实现方式,分为以下四种:

4.1 实例方法

  • Java实例方法同步是在拥有该方法的对象上。每个实例其方法同步都会在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。
    public synchronized void add() {
        for(int i=0;i<100;++i) {
            count += 1;
            System.out.println(this.getName() + ":" + count);
        }
    }

4.2 静态方法同步

  • 同步在该方法所在的类对象上,因为在Java虚拟机中一个类只能对应一个类对象,因此同时只允许一个线程执行同一个类中的静态同步方法。
    public static synchronized void add() {
        for(int i=0;i<100;++i) {
            count += 1;
            System.out.println(":" + count);
        }
    }

4.3 实例方法同步块

    public void run() {
        for(int i=0;i<100;++i) {
            synchronized(this){
                count += 1;
                System.out.println(this.getName() + ":" + count);
            }
        }
    }
  • Java同步块构造器用括号将对象括起来。在上面案例中,使用了"this",即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。一次只有一个线程能够在同步与同一个监视器对象的Java方法内执行。
  • 下面两个例子都同步所调用的实例对象上,因此它们在同步的执行效果上是等效的。
public class MyThread {

    public synchronized void log1(String msg1,String msg2){
        System.out.println(msg1);
        System.out.println(msg2);
    }

    public  void log2(String msg1,String msg2){
        synchronized(this) {
            System.out.println(msg1);
            System.out.println(msg2);
        }
    }

}

4.4 静态方法同步块

  • 以下两种方法不允许同时被线程访问。如果第二个同步块不是同步在MyThread.class这个对象上,那么这两个方法可以同时被线程访问。
public class MyThread {

    public static synchronized void log1(String msg1,String msg2){
        System.out.println(msg1);
        System.out.println(msg2);
    }

    public static void log2(String msg1,String msg2){
        synchronized(this) {
            System.out.println(msg1);
            System.out.println(msg2);
        }
    }

}

五、Java线程通信

  • 线程通信的目的在于使线程间能够互相发送信号。并且线程通信能够等待其他线程的信号。
  • Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object类定义了三个方法,wait( )、notify( )、notifyAll( )来实现这个等待机制。
  • 一个线程一旦调用了任意对象的wait( )方法,就会变成为非运行状态,直到另外一个线程调用了同一个对象的notify( ) 方法。为了调用wait( )或者notify( ),线程必须先获得那个对象的锁。也就说,线程必须在同步块里调用wait( )或者notify( )。
public class MyWaitNotify {
    private final Object myMonitor=new Object();
    private boolean signaled=false;
    
    public void doWait(){
        synchronized(myMonitor){
            while(!signaled) {
                try {
                    myMonitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            signaled=false;
        }
    }
    
    public void doNotify(){
        synchronized(myMonitor){
            signaled=true;
            myMonitor.notify();
        }
    }
}

从上面案例可以推断:

  1. 不管是等待线程还是唤醒线程都在同步块调用wait( ) 和notify( )。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait( ),notify( )或者notifyAll( )。否则,会抛出IllegalMonitorStateException异常;
  2. 一旦线程调用了wait( )方法,就会释放了所持有的监视器对象上的锁,这将允许其他线程可以调用wait( )或者notify( );
  3. 为了避免丢失信号,必须将它们保存在信号类里面,如上面signalled变量中;
  4. 假唤醒:由于莫名其妙的原因,线程有可能在没有调用过notify( )和notifyAll( )的情况下醒来,这就是所谓的假唤醒(spurious wakeups)。为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里,这样的一个while循环叫做自旋锁;
  5. 不要在字符串常量或者全局对象中调用wait( ),即上面myMonitor不能是字符串常量或者全局对象。每一个MyWaitNotify的实例都有一个属于自己的监听器对象,而不是在空字符串上调用wait( )或者notify( )。

六、Java锁

Java自带一些锁,并不需要程序员去实现。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

一个可重入锁(reentrant lock)的简单实现,

public class MyLock {
    private boolean isLocked=false;
    private Thread lockedBy;
    private int lockedCount;
    
    public synchronized void lock() throws InterruptedException {
        Thread callingThread=Thread.currentThread();
        while(isLocked&&lockedBy!=callingThread){
            wait();
        }
        isLocked=true;
        lockedCount++;
        lockedBy=callingThread;
    }
    
    public synchronized void unlock() {
        if(Thread.currentThread()==this.lockedBy){
            lockedCount--;
            if(lockedCount==0){
                isLocked=false;
                notify();
            }
        }
    }
}

注意记得要释放锁unlock( )

lock.lock( );
try{
    work();
) finally {
    lock.unlock( );
}

七、Java的其他同步方法

  • 信号量(semaphore):import java.util.concurrent.Semaphore
  • 阻塞队列(Blocking Queue):import java.util.concurrent.BlockingQueue
import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {
    private List queue=new LinkedList<>();
    private int limit=0;
    
    public BlockingQueue(int limit){
        this.limit=limit;
    }
    
    public synchronized void enqueue(Object obj) throws InterruptedException {
        while(queue.size()==limit){
            wait();
        }
        if(queue.size()==0){
            notifyAll(); //唤醒所有线程
        }
        queue.add(obj);
    }
    
    public synchronized Object dequeue() throws InterruptedException {
        while(queue.size()==0){
            wait();
        }
        if(queue.size()==limit){
            notifyAll();
        }
        return queue.remove(0);
    }
}

八、Java的线程池

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool:创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程。当任务数增加时,此线程池又可以添加新的线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(JVM)能够创建的最大线程大小;
  • newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就会创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新的线程;
  • newScheduledThreadPool:创建一个大小不限制的线程池,此线程池支持定时以及周期性执行任务;
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一线程因为异常结束,那么会有一个新的线程来替换它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyPool {
    public static void main(String[] args){
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for(int i=0;i<10;++i){
            final int idx=i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(idx);
                }
            });
        }
    }
}

猜你喜欢

转载自blog.csdn.net/Jiangtagong/article/details/123643150