JAVA多线程机制

参考<<疯狂JAVA讲义>> 第16章节,还可以参考sina博文 "JAVA多线程相关介绍"

多线程概述
个人觉得这方面已经有很多资料介绍了,不过还是觉得 <<疯狂JAVA讲义>>线程概述还是挺透彻,明了的

2种方式创建线程,一种是extends Thread,一种是implements Runnable
这里需要注意的是Runnable对象仅仅是作为Thread对象的target,Runnable的run方法作为仅是线程执行体

线程的生命周期(这里疯狂JAVA讲义在这块讲的很好)

和线程控制有关的方法:
start():新建的线程进入Runnable状态.
run():线程进入Running状态(不要直接在程序中调用线程的run方法,是由系统调用的)。
wait():线程进入等待状态,等待被notify,这是对象方法,而不是线程方法
notify()/notifyAll():唤醒其他线程,这是对象方法,而不是线程方法
yield():线程放弃执行,使其他优先级不低于该线程的线程有机会运行,是静态方法
getPriority()/setPriority():设置线程优先级
sleep():线程睡眠一段时间
join():调用这个方法的主线程,会等待加入的子线程完成。

多线程同步
A:关键字synchronized 来与对象的互斥锁联系
1)synchronized关键字可以修饰代码块,方法,但是不可以修饰构造器,属性等。
2)特别指出的是synchronized锁定的不是方法或代码块,而是对象。当synchronized作为方法的修饰符时,它所取得的对象锁将被转交给方法的调用者;
当synchronized修饰的是对象时,则取得的对象锁将被转交给该引用指向的对象。Synchronized也可以修改类,表示这个类的所有方法都是Synchronized的
3)释放同步监视器(synchronized)的锁定:
当线程执行到synchronized()块结束时,释放对象锁
当在synchronized()块中遇到break, return或抛出exception,则自动释放对象锁
当一个线程调用wait()方法时,它放弃拥有的对象锁并进入blocked 状态

public class SuperTest 
{
    public static void main(String[] str)
    {
        Counter ct = new Counter();
        C1 c1 = new C1(ct, " c1 ");
        C1 c2 = new C1(ct, " c2 ");
        
        //启动2个子线程
        c2.start();
        c1.start();
        
        try {
            c2.join();
            c1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(ct.getCount());
    }
}
    class Counter {
        private int count = 1000;

        protected int getCount() {
            return count;
        }

        protected void setCount(int count) {
            this.count = count;
        }
    }

    class C1 extends Thread {
        private Counter counter;

        private String sname;

        public C1(Counter cc, String s1) {
            this.counter = cc;
            this.sname = s1;
        }

        public Counter getCounter() {
            return counter;
        }

        public void setCounter(Counter counter) {
            this.counter = counter;
        }

        public String getSname() {
            return sname;
        }

        public void setSname(String sname) {
            this.sname = sname;
        }

        public void run() {
            for (int j = 0; j < 100; j++) {
                synchronized (counter){
                int i = counter.getCount();
                System.out.println(sname + j +  " before " + i);
                //System.out.println("now in :" + sname + " count = " + i);
                
                AddCounter(i - 1);
                    System.out.println(sname + j +  " after " + counter.getCount());
                }
            }
        }
        
        public void AddCounter(int i) {
            counter.setCount(i);
        }
    }
    

 

B:同步锁Lock

public class TestDraw
{
    public static void main(String[] args) 
    {
        //创建一个账户
        Account acct = new Account("1234567" , 1000);
        //模拟两个线程对同一个账户取钱
        new DrawThread("甲" , acct , 800).start();
        new DrawThread("乙" , acct , 800).start();
    }
}

public class DrawThread extends Thread
{
    //模拟用户账户
    private Account account;
    //当前取钱线程所希望取的钱数
    private double drawAmount;

    public DrawThread(String name , Account account , 
        double drawAmount)
    {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    //当多条线程修改同一个共享数据时,将涉及到数据安全问题。
    public void run()
    {
        account.draw(drawAmount);
    }
}

public class Account
{
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    private String accountNo;
    private double balance;


    public Account(){}

    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
         return this.accountNo;
    }

    public double getBalance()
    {
         return this.balance;
    }
    public void draw(double drawAmount)
    {
        lock.lock();
        try
        {
            //账户余额大于取钱数目
            if (balance >= drawAmount)
            {
                //吐出钞票
                System.out.println(Thread.currentThread().getName() + 
                    "取钱成功!吐出钞票:" + drawAmount);
                try
                {
                    Thread.sleep(1);            
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                //修改余额
                balance -= drawAmount;
                System.out.println("\t余额为: " + balance);
            }
            else
            {
                System.out.println(Thread.currentThread().getName() +
                    "取钱失败!余额不足!");
            }            
        }
        finally
        {
            lock.unlock();
        }
    }

    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if (obj != null && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

 

线程通信
(1) 线程的协调运行
wait(),notify(),notifyAll().这3个方法是Object类的方法,而不是Thread的方法,而且这3个方法必须由同步监视器(synchronized)对象来调用,2种情况:
1.synchronized修饰同步方法,因为该类默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这3个方法
2.synchronized修饰同步代码块,同步监视器是synchronized括号中的对象,所以必须使用该对象调用这3个方法

wait()方法:
1. wait()方法是Object对象的方法,而不是Thread的方法
2. wait()方法只可能在synchronized块中被调用
3. wait()被调用时,原来的锁对象打开锁,线程进入blocked状态
4. wait()时间到期或被notify()唤醒的线程从wait()后面的代码开始继续执行
5. wait()和sleep()的主要区别是wait()会释放对象锁,而sleep()不会。

notify()和notifyAll():
1.只能在synchronized中被调用
2.notify它会唤起同一个锁对象上的一个等待线程.但如果有几个线程在等待列表中,它无法决定是哪一个线程被唤醒。所以,为了防止不该唤醒的线程被唤醒,应该调用notifyAll,让所有的等待线程都有机会运行。

public class SuperTest 
{
    public static void main(String[] str)
    {
        Counter ct = new Counter();
        C2 c2 = new C2(ct, " c2 ");
        C1 c1 = new C1(ct, " c1 ");
        C2 c3 = new C2(ct, " c3 ");
        C1 c4 = new C1(ct, " c4 ");
        c3.start();
        c4.start();
        c2.start();
        c1.start();
        try {
            c2.join();
            c1.join();
            c3.join();
            c4.join();

        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(ct.getCount());
    }
}

class Counter {
    private int count = 1000;

    public   int getCount() {
        return count;
    }

    public  void setCount(int count) {
        this.count = count;
    }
    
    public synchronized void addCounter(int j,String sname) {
        while (this.getCount() > 1007){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
                
        int i = this.getCount();
        System.out.println(sname + j +  " before " + i);
        //System.out.println("now in :" + sname + " count = " + i);
        
        this.setCount(i+1);
        this.notifyAll();
        System.out.println(sname + j +  " after " + this.getCount());
    }    
    
    public synchronized void delCounter(int j,String sname) {
        while (this.getCount() < 990){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
                
        int i = this.getCount();
        System.out.println(sname + j +  " before " + i);
        //System.out.println("now in :" + sname + " count = " + i);
        this.setCount(i - 1);
        this.notifyAll();
        System.out.println(sname + j +  " after " + this.getCount());
    }    
}

class C1 extends Thread {
    private Counter counter;

    private String sname;

    public C1(Counter cc, String s1) {
        this.counter = cc;
        this.sname = s1;
    }

    public Counter getCounter() {
        return counter;
    }

    public void setCounter(Counter counter) {
        this.counter = counter;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public void run() {
        for (int j = 0; j < 100; j++) {
            /*synchronized (counter){
            delCounter(j);
            }
            */
            counter.delCounter(j,sname);
        }
    }
}

class C2 extends Thread {
    private Counter counter;
    private String sname;
    
    public C2(Counter cc, String s1) {
        this.counter = cc;
        this.sname = s1;
    }

    public Counter getCounter() {
        return counter;
    }

    public void setCounter(Counter counter) {
        this.counter = counter;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public void run() {
        for (int j = 0; j < 100; j++) {
            /*
            synchronized (counter) {
            addCounter(j);
            }
            */
            if (j % 19 == 0) 
            { 
                try {
                        sleep(0,100);
                    } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                    }
            }
            
            counter.addCounter(j,sname);
        }
    }
}

这里使用的while作为判断语句,而不是if,这就是多线程中的“旋锁spin lock”的概念。这样可以避免用if带来的问题(仔细想想),是这样的,我们在一个线程被唤醒时候要重新检测它的等待条件,一般用旋锁来实现。


我们可以和以前我们做项目用的UDI的线程处理来做对比,以前我们要保证共享数据操作的完整性,实现数据的同步时需要我们自己创建互斥量CSUDIOSMutexCreate,然后自己主动调用CSUDIOSMutexWait和CSUDIOSMutexRelease等待和释放互斥量来实现,而JAVA直接使用synchronized来实现了UDI互斥量类似的功能。
JAVA来用wait,notify,notifyAll来实现线程之间的通信,我们以前一般没有类似的线程通信,有一个比较类似的做法,比如要我们要结束某个线程,一般都是定义一个全局变量,修改其FLAG,然后调用CSUDIOSThreadJoin等待其结束,而在实际的线程函数中一般会用一个while循环不断检测该全局变量的flag,以便可以及时结束线程。

(2) 使用条件变量控制协调
如果程序不使用synchronized来保存同步,而是直接使用Lock对象来保证同步,那么系统中不存在隐式的同步监视器对象,也就不能使用
wait(),notify(),notifyAll()实现同步了,那么我们可以利用条件变量Condition来实现。

public class Account
{
    //显示定义Lock对象
    private final Lock lock = new ReentrantLock();
    //获得指定Lock对象对应的条件变量
    private final Condition cond  = lock.newCondition(); 

    private String accountNo;
    private double balance;

    //标识账户中是否已经存款的旗标
    private boolean flag = false;

    public Account(){}

    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
         return this.accountNo;
    }

    public double getBalance()
    {
         return this.balance;
    }
    public void draw(double drawAmount)
    {
        //加锁
        lock.lock();
        try
        {
            //如果账户中还没有存入存款,该线程等待
            if (!flag)
            {
                cond.await();
            }
            else
            {
                //执行取钱操作
                System.out.println(Thread.currentThread().getName() + 
                    " 取钱:" +  drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                //将标识是否成功存入存款的旗标设为false
                flag = false;
                //唤醒该Lock对象对应的其他线程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        //使用finally块来确保释放锁
        finally
        {
            lock.unlock();
        }
    }
    public void deposit(double depositAmount)
    {
        lock.lock();
        try
        {
            //如果账户中已经存入了存款,该线程等待
            if(flag)
            {
                cond.await();                
            }
            else
            {
                //执行存款操作
                System.out.println(Thread.currentThread().getName() + 
                    " 存款:" +  depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                //将标识是否成功存入存款的旗标设为true
                flag = true;
                //唤醒该Lock对象对应的其他线程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        //使用finally块来确保释放锁
        finally
        {
            lock.unlock();
        }
    }

    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if (obj != null && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

 

(3) 使用管道流
管道流的3种存在形式:
PipedInputStream和PipedOutputStream,PipedReader和PipedWriter,Pipe.SinkChannel和Pipe.SourceChannel

通常没有必要使用管道流来控制2个线程间通信,因为2个线程属于同一个进程,它们可以非常方便的共享数据,不必用管流

class ReaderThread extends Thread
{
    private PipedReader pr;
    //用于包装管道流的BufferReader对象
    private BufferedReader br;
    public ReaderThread(){}
    public ReaderThread(PipedReader pr)
    {
        this.pr = pr;
        this.br = new BufferedReader(pr);
    }
    public void run()
    {
        String buf = null;
        try
        {
            //逐行读取管道输入流中的内容
            while ((buf = br.readLine()) != null)
            {
                System.out.println(buf);
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
        //使用finally块来关闭输入流
        finally
        {
            try
            {
                if (br != null)
                {
                    br.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
}
class WriterThread extends Thread
{
    String[] books = new String[]
    {
        "Struts2权威指南",
        "ROR敏捷开发指南",
        "基于J2EE的Ajax宝典",
        "轻量级J2EE企业应用指南"
    };
    private PipedWriter pw;
    public WriterThread(){}
    public WriterThread(PipedWriter pw)
    {
        this.pw = pw;
    }
    public void run()
    {
        try
        {
            //循环100次,向管道输出流中写入100个字符串
            for (int i = 0; i < 100 ; i++)
            {
                pw.write(books[i % 4] + "\n");
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
        //使用finally块来关闭管道输出流
        finally
        {
            try
            {
                if (pw != null)
                {
                    pw.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
}

public class PipedCommunicationTest
{
    public static void main(String[] args)
    {
        PipedWriter pw = null;
        PipedReader pr = null;
        try
        {
            //分别创建两个独立的管道输出流、输入流
            pw = new PipedWriter();
            pr = new PipedReader();
            //连接管道输出流、出入流
            pw.connect(pr);

            //将连接好的管道流分别传入2个线程,
            //就可以让两个线程通过管道流进行通信
            new WriterThread(pw).start();
            new ReaderThread(pr).start();
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

 

JAVA线程池:(参考疯狂JAVA编程对线程池的介绍)

为什么要用线程池:
1)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务 2)可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,
而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

几个重要的类:
ExecutorService:真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

Executors类里面提供了一些静态工厂,生成一些常用的线程池。
newSingleThreadExecutor:
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool:
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool:
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,
当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,
线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool:
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

newSingleThreadExecutor:
创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

那我个人感觉就是new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEPALIVE_TIME, TIME_UNIT, workQueue, rejectedExecutionHandler);
提供了更定制化的线程池制造方法。因为newFixedThreadPool方法其实也是return new ThreadPoolExecutor

例子一:

//实现Runnable接口来定义一个简单的
class TestThread implements Runnable
{
    public void run()
    {
        for (int i = 0; i < 100 ; i++ )
        {
            System.out.println(Thread.currentThread().getName()
                + "的i值为:" + i);
        }
    }
}

public class ThreadPoolTest
{
    public static void main(String[] args) 
    {
        //创建一个具有固定线程数(6)的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        //向线程池中提交2个线程
        pool.submit(new TestThread());
        pool.submit(new TestThread());
        //关闭线程池
        pool.shutdown();
    }
}

例子二:

public class ThreadPoolTask implements Runnable { 
  // 保存任务所需要的数据 
  private Object threadPoolTaskData; 
  private static int consumeTaskSleepTime = 2000; 

  ThreadPoolTask(Object tasks) { 
    this.threadPoolTaskData = tasks; 
  } 

  public void run() { 
    // 处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句 
    System.out.println("start .." + threadPoolTaskData); 
    try { 
      //便于观察,等待一段时间 
      Thread.sleep(consumeTaskSleepTime); 
    } catch (Exception e) { 
      e.printStackTrace(); 
    } 
    threadPoolTaskData = null; 
  } 

  public Object getTask() { 
    return this.threadPoolTaskData; 
  } 
} 

public class ThreadPool { 
  private static int produceTaskSleepTime = 2; 
  private static int consumeTaskSleepTime = 2000; 
  private static int produceTaskMaxNumber = 10; 

  public static void main(String[] args) { 
    // 构造一个线程池 
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3, 
        TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3), 
        new ThreadPoolExecutor.DiscardOldestPolicy()); 

    for (int i = 1; i <= produceTaskMaxNumber; i++) { 
      try { 
        // 产生一个任务,并将其加入到线程池 
        String task = "task@ " + i; 
        System.out.println("put " + task); 
        threadPool.execute(new ThreadPoolTask(task)); 

        // 便于观察,等待一段时间 
        Thread.sleep(produceTaskSleepTime); 
      } catch (Exception e) { 
        e.printStackTrace(); 
      } 
    } 
  } 
} 

 

多线程的一般规则:
1. 如果2个或以上的线程都修改一个对象,那么把执行修改的方法定义为同步的,如果对象更新影响到只读方法,那么只读方法也要定义成同步的。
2. 如果一个线程必须等待一个对象的状态发生变化,那么它应该在对象内部等待,而不是在外部。它可以调用一个被同步的方法,并让这个方法调用wait()
3. 每当一个方法返回某个对象的锁时,它应该调用notify()/notifyAll()来让等待中的其他线程有机会调用。
4. 有wait()在的地方必须相应的有notify/notifyAll方法,且他们都作用于同一对象
5. 针对wait/notify/notifyAll使用旋锁(spin lock)
6. 优先使用notifyAll,而不是notify
7. 按照固定的顺序获取对象锁,以免死锁
8. 不要对上锁的对象改变他的引用
9. 不要滥用同步机制,避免无谓的同步控制

 

posted on 2013-03-22 17:38  Cynthia&Sky  阅读(709)  评论(0编辑  收藏  举报

导航