Java 多线程

一:多线程的优缺点

        多线程的优点:
             1、有效的占用了cpu的空闲时间,从一定程度上提高了效率
            2、提高了用户的体验性
            3、将一个复杂的进程拆分成若干个小的线程,提高代码的分离性和维护性
         多线程的缺点:
            1、设计更复杂, 虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。
            在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。
            不正确的线程同步产生的错误非常难以被发现,并且重现以修复。
            2、上下文切换的开销, 当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,
            然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。
            CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。



二:创建多线程

            在Java中,多线程的实现有两种方式:

                 1、继承 java.lang.Thread类
public class TestMitiThread {
    public static void main(String[] rags) {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        new MitiSay("A").start();
        new MitiSay("B").start();
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
}
class MitiSay extends Thread {
    public MitiSay(String threadName) {
        super(threadName);
    }
    public void run() {
        System.out.println(getName() + " 线程运行开始!");
        for (int i = 0; i < 10; i++) {
            System.out.println(i + " " + getName());
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(getName() + " 线程运行结束!");
    }
}
运行结果:
main 线程运行开始!
main 线程运行结束!
A 线程运行开始!
0 A
1 A
B 线程运行开始!
2 A
0 B
3 A
4 A
1 B
5 A
6 A
7 A
8 A
9 A
A 线程运行结束!
2 B
3 B
4 B
5 B
6 B
7 B
8 B
9 B
B 线程运行结束!
        注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。


                 2、 实现java.lang.Runnable接口
public class TestMitiThread1 implements Runnable {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        TestMitiThread1 test = new TestMitiThread1();
        Thread thread1 = new Thread(test);
        Thread thread2 = new Thread(test);
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 10; i++) {
            System.out.println(i + " " + Thread.currentThread().getName());
            try {
                Thread.sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
}
运行结果:
main 线程运行开始!
Thread-0 线程运行开始!
main 线程运行结束!
0 Thread-0
Thread-1 线程运行开始!
0 Thread-1
1 Thread-1
1 Thread-0
2 Thread-0
2 Thread-1
3 Thread-0
3 Thread-1
4 Thread-0
4 Thread-1
5 Thread-0
6 Thread-0
5 Thread-1
7 Thread-0
8 Thread-0
6 Thread-1
9 Thread-0
7 Thread-1
Thread-0 线程运行结束!
8 Thread-1
9 Thread-1
Thread-1 线程运行结束!

        注意:TestMitiThread1类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。


    实现Runnable接口比继承Thread类所具有的优势:
        1):适合多个相同的程序代码的线程去处理同一个资源
        2):可以避免java中的单继承的限制
        3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
        4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类


三:常见方法

    currentThread 获取当前线程对象
    setName设置线程名称
    getName获取线程名称
    setPriority设置线程优先级1——10
    getPriority获取线程优先级
    sleep休眠         使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
    interrupt中断休眠状态
    yield线程的礼让     暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
    join线程的插队     等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
    setDaemon设置守护线程
    wait   Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
    notify       Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个                 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定                此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。


    调整线程优先级:
        Java线程有优先级,优先级高的线程会获得较多的运行机会。
        Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
        static int MAX_PRIORITY           线程可以具有的最高优先级,取值为10。
        static int MIN_PRIORITY           线程可以具有的最低优先级,取值为1。
        static int NORM_PRIORITY           分配给线程的默认优先级,取值为5。
        Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
        每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
        线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。


    部分方法演示 使 用:
    
* yield:线程的礼让,但是礼让的时间不确定。 是一个静态方法,直接通过类名调用即可
* join:线程的插队,当前线程已经抢到cpu占用权,让其他线程插队在自己前面执行
*                 如果其他线程插队成功,则肯定其他线程先执行完
*                 注意:其他线程有可能没有插队成功
*
*/

public class TestThreadMethod4 {
    
    public static void main(String[] args) {
        
//        YieldDemo yd = new YieldDemo();
//        yd.start();
        
        JoinDemo jd = new JoinDemo();
        jd.start();
        for(int i=1;i<=500;i++){
            System.out.println("马兰伟在买包子~~"+i);
            if(i>=5){
                try {
                    jd.join();//让jd线程插队在当前线程前面去,先执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        }
        
    }

}
class JoinDemo extends Thread{
    
    @Override
    public void run() {
        for(int i=1;i<=100;i++){
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("王腾飞要买包子~~"+i);
        }
        
    }
}
class YieldDemo extends Thread{
    
    @Override
    public void run() {
        long start = System.currentTimeMillis();
        
        String str="";
        for(int i=1;i<=5000;i++){
            str+="hello"+i;
            Thread.yield();//让当前线程礼让
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时时间:"+(end-start));
    }
    
}

        sleep()和yield()的区别:
            sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
            yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

            sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。
            实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。
            所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程

            另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,
            所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,
            又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。


* sleep:让当前线程休眠指定毫秒数,参数单位:ms,是一个静态方法,可以直接通过类名调用。一般来讲,用于模拟线程交错效果
* interrupt:中断线程的休眠、等待状态,如果中断的线程正在休眠,则会抛InterruptedException
*
*/
public class TestThreadMethod2 {
    
    public static void main(String[] args) {
        
        System.out.println("main:"+Thread.currentThread().getPriority());
        SleepThread st = new SleepThread();
        st.start();
        System.out.println("st:"+st.getPriority());
        
        for(int i=1;i<=100;i++){
            System.out.println("岳灵珊小师妹在练剑"+i);
            if(i==10){
                st.interrupt();
                break;
            }
        }
    }

}

class SleepThread extends Thread{
    
    @Override
    public void run() {
        
        for(int i=1;i<=50;i++){
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
//                e.printStackTrace();
                System.out.println("令狐冲睡醒啦,开始好好学习");
                break;
            }
            
        }
        
    }
}

Interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
   需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。
但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。


四:线程状态转换

 


     1、新建状态(New):新创建了一个线程对象。
    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。            (注意:sleep是不会释放持有的锁)
    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。




五:线程同步
    
      线程安全:
        在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这            些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
        当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
      对象+synchronized  搭配使用
    方式一:同步代码块
        synchronized(锁对象){
            //需要上锁的代码(同步的代码)
        }

    方式二:同步方法
        public synchronized 返回类型 方法名(参数列表){
            //需要上锁的代码(同步的代码)
        }

        注意:
            普通的同步方法,锁对象是:this
            静态的同步方法,锁对象是:当前类.class

        要求:  多个线程使用的锁对象必须是同一个!!!

     同步的前提:
    1、多线程
    2、有共享资源&线程的任务体中有对共享资源处理语句

    卖票实现票数的统一:
public class TestSynchronized1 {
    
    public static void main(String[] args) {
        SellTicket2 s = new SellTicket2();
        Thread t1 = new Thread(s);
        t1.setName("窗口A");
        t1.start();
        
        Thread t2 = new Thread(s);
        t2.setName("窗口B");
        t2.start();
        
        
        Thread t3 = new Thread(s);
        t3.setName("窗口C");
        t3.start();
        
        
        
    }

}
//方式二:使用同步方法实现同步
class SellTicket2 implements Runnable{
     int tickets = 100;//总票数
    
     boolean loop=true;
        @Override
        public void run() {
            while(loop){
                sellTicket();
                    
            }
            
        }
        private synchronized void sellTicket() {
            if (tickets <= 0) {
                System.out.println("票已经售完!");
                loop=false;
                return;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖了一张票,票数余额:" + (--tickets));
        }
        
        
}

//方式一:使用同步代码块实现同步
class SellTicket implements Runnable{
     int tickets = 100;//总票数
        @Override
        public void run() {
            while(true){
                
                synchronized (this) {
                    if (tickets <= 0) {
                        System.out.println("票已经售完!");
                        break;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖了一张票,票数余额:" + (--tickets));
                }
            }
            
        }
}



六:线程通信

         线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
        Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。

        一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。

以下为一个使用了wait()和notify()实现的线程间通信的共享对象
        
public class TestCommunication {
    public static void main(String[] args) {
        WithDraw w1 = new WithDraw();
        Thread t1 = new Thread(w1);
        t1.setName("周芷若");
        t1.start();

        Thread t2 = new Thread(w1);
        t2.setName("赵敏");
        t2.start();
    }

}

class WithDraw implements Runnable {
    int money = 10000;

    @Override
    public void run() {

        while (true) {
            synchronized (this) {
                if (money <= 0) {
                    System.out.println("钱已经取完");
                    break;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "取了1000,还剩余:" + (money -= 1000));
                
                //通知对方线程可以取钱了
                this.notify();
                
                //当前线程等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

    }

}

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



七:线程单例模式的双重校验锁

加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它 存在着性能问题, 依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,先看下它的实现代码

1. public class Singleton {  
2.     private static Singleton instance = null;  
3.     private Singleton(){}  
4.     public static Singleton getInstance() {  
5.         if (instance == null) {  //1   提高效率
6.             synchronized (Singleton.class) {  
7.                 if (instance == null) { //2  线程安全
8.                     instance = new Singleton();  
9.                 }  
10.             }  
11.         }  
12.         return instance;  
13.     }  
14. }  
        可以看到上面在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码2。
       我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
       这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
       这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
       以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。代码如下
1. public class Singleton {  
2.     private static volatile Singleton instance = null;  
3.     private Singleton(){}  
4.     public static Singleton getInstance() {  
5.         if (instance == null) {  
6.             synchronized (Singleton.class) {  
7.                 if (instance == null) {  
8.                     instance = new Singleton();  
9.                 }  
10.             }  
11.         }  
12.         return instance;  
13.     }  
14. }  



八:线程经典问题(生产者与消费者)


/**
 * 此类用于演示线程的通信
 *
 * 案例:多个生产者和多个消费者
 * 修改:
 * ①notify——>notifyAll
 * ②wait的条件判断,改成循环判断
 *
 *
 */
public class Test {
     public static void main(String[] args) {
         Clerk2 c = new Clerk2();
         Productor2 p1 = new Productor2(c, "张无忌");
         p1.start();
         Productor2 p2 = new Productor2(c, "令狐冲");
         p2.start();
         Consumer2 con1 = new Consumer2(c, "赵敏");
         con1.start();
         Consumer2 con2 = new Consumer2(c, "小昭");
         con2.start();
         Consumer2 con3 = new Consumer2(c, "依琳小师妹");
         con3.start();
     }
}
class Consumer2 extends Thread {
     private Clerk2 c;
     public Consumer2(Clerk2 c, String name) {
         super(name);
         this.c = c;
     }
     @Override
     public void run() {
         while (true) {
              // 消费产品(调用Clerk对象的get方法)
              c.get();
         }
     }
}
class Productor2 extends Thread {
     private Clerk2 c;
     public Productor2(Clerk2 c, String name) {
         super(name);
         this.c = c;
     }
     @Override
     public void run() {
         while (true) {
              // 生产产品(调用Clerk对象的save方法)
              c.save();
         }
     }
}
class Clerk2 {
     int count = 0;// 产品数量
     // 生产
     public synchronized void save() {// 默认锁对象:this
         while (count >= 20) {// 库存已满
              // 等待
              try {
                  this.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
         }
         try {
              Thread.sleep(10);
         } catch (InterruptedException e) {
              e.printStackTrace();
         }
          System.out.println(Thread.currentThread().getName() + "生产了一件产品,目前库存为:"
                  + (++count));
         this.notifyAll();// 唤醒其他正在等待的所有线程
     }
     // 消费
     public synchronized void get() {
         while (count <= 0) {
              try {
                  this.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
         }
         try {
              Thread.sleep(10);
         } catch (InterruptedException e) {
              e.printStackTrace();
         }
          System.out.println(Thread.currentThread().getName() + "消费了一件产品,目前库存为:"
                  + (--count));
         this.notifyAll();// 唤醒其他正在等待的所有线程
     }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值