• Java 多线程-上课总结


    一、操作系统中线程和进程的概念

    现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。

    进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

    线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

    “同时”执行是人的感觉,在线程之间实际上轮换执行。

     

    二、Java中的线程

    在Java中,使用java.lang.Thread类或者java.lang.Runnable接口编写代码来定义、实例化和启动新线程。 

    一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。 

    Java中,每个线程都有一个调用栈,即使不在程序中创建任何新的线程,线程也在后台运行着。 

    一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。 

    一旦创建一个新的线程,就产生一个新的调用栈。 

    线程总体分两类:用户线程和守候线程。

    当所有用户线程执行完毕的时候,JVM自动关

     

    三、定义线程

    1、扩展java.lang.Thread类。

    复制代码
    //继承Thread类,重写Thread类的run方法。
    public class Thread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
          //循环执行1000次。输出当前线程名称。
          System.out.println(Thread.currentThread().getName() + "正在运行!");
            }
        }
    }        
    复制代码

    调用:

    Thread t1 = new Thread1();
    t1.start();

    2、实现java.lang.Runnable接口。

    复制代码
    //实现Runnable接口,重写run方法
    public class Thread2 implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //循环输出1000次当前线程名称
           System.out.println(Thread.currentThread().getName() + "正在运行!");
            }
        }
    }
    复制代码

    调用:

    Thread t2 = new Thread(new Thread2());  //将实现runnable接口的类作为参数传递给Thread构造方法
    t2.start();

     线程中所存在的问题:

    1、线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。

    2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。

    3、获取当前线程的对象的方法是:Thread.currentThread();

    4、在上面的代码中,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。

    5、当线程目标run()方法结束时该线程完成。

    6、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。

    7、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。

    众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。

    8、尽管通常采用队列形式,但这是没有保障的。队列形式是指当 一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可 运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。

    9、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

     

      继承Thread类和继承Runnable接口实现多线程的区别:

      1、继承Thread类,优点:编写较为简单,可以使用this直接访问当前线程的对象。缺点:由于Java中不支持多继承,所以如果一个类已经有一个父类的情况下,就无法再继承Thread类了。

      2、继承Runnable接口,优点:可以继承其他的类,多个线程之间可以使用同一个Runnable对象。实现了线程之间资源的共享。缺点:相对于第一种稍微复杂一些,如果访问当前线程,需要使用Thread.currentThread()方法。

     

    四、线程状态

    线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是生、死、可运行、运行、等待/阻塞。用一个图来描述如下:

    1、新状态:线程对象已经创建,还没有在其上调用start()方法。此时线程仅仅是一个空对象,还没有分配资源,此时只能调用线程的启动和终止方法,任何其他的操作都会引发异常。

    2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。

    3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

    4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。

    5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的, 但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出 java.lang.IllegalThreadStateException异常。

     

    五、线程的优先级

      线程的优先级用1-10表示,1表示最低,10表示最高,默认为5。

      线程优先级可以通过setPriority(int grade)方法更改,此方法的参数表示要设置的优先级,他必须是1-10之间的整数,如果超出这个值,则会抛出 IllegalArgumentException异常。虽然设置了优先级,但是不一定就一定会执行,还是需要根据cpu调度来执行,只是被执行的几率较 大。

     

    六、线程调度方法

      1、join()

      join表示暂停当前线程,将其他线程插入当前线程中执行,等待其他线程执行完毕后再执行当前线程。

      例:

      定义线程类thread1

    复制代码
    public class Thread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "正在运行!");
            }
        }
    }
    复制代码

      定义测试类,main方法

    复制代码
    public static void main(String[] args) {
        //定义线程t1
        Thread t1 = new Thread1();
        t1.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "正在运行!");
            try {
                t1.join();  //将线程t1插入到主线程中执行,主线程暂停执行,等待t1执行完毕后,主线程开始执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("程序结束");
    }
    复制代码

    执行结果:

    复制代码
    main正在运行!
    Thread-0正在运行!
    Thread-0正在运行!
    Thread-0正在运行!
    Thread-0正在运行!
    Thread-0正在运行!
    main正在运行!
    main正在运行!
    main正在运行!
    main正在运行!
    程序结束
    复制代码

      2、sleep()

      sleep(millis)方法会让当前线程睡眠(阻塞状态)millis毫秒,睡眠时间过后线程会再次进入可执行状态。

      例:

    复制代码
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "正在运行!");
            try {
                Thread.sleep(1000);  //让主线程每1秒执行一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("程序结束");
    }
    复制代码

      3、yield()

      yield()方法可暂停当前线程,允许其他线程执行,该线程仍处于可运行状态,不转为阻塞状态。

    复制代码
    public static void main(String[] args) {
        Thread t1 = new Thread1();
        t1.start();
        
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "正在运行!");
            t1.yield();  //让t1线程处于暂停状态,暂停后也可能重新被激活。
        }
        System.out.println("程序结束");
    }
    复制代码

    七、线程同步

      当两个或多个线程访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用的方式成为线程同步。

      线程同步分为两种方式:同步方法和同步代码块。这两种方式都使用synchronized实现。

      下面通过火车票为例说明线程同步问题。

      火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。比如下面程序:

    TicketThread 售票窗口:
    复制代码
    public class TicketThread implements Runnable {
        // 票的总数
        private int ticket = 10;
    
        @Override
        public void run() {
            for (int i = 1; i < 50; i++) {
                // 休眠1s秒中,为了使效果更明显,否则可能出不了效果
                if (ticket > 0) {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "号窗口卖出" + this.ticket-- + "号票");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    复制代码

      mian方法:

    public static void main(String[] args) {
        TicketThread tt = new TicketThread();
        new Thread(tt,"thread_a").start();
        new Thread(tt,"thread_b").start();
        new Thread(tt,"thread_c").start();
    }

      执行结果

    复制代码
    thread_b号窗口卖出8号票
    thread_c号窗口卖出10号票
    thread_a号窗口卖出9号票
    thread_b号窗口卖出7号票
    thread_a号窗口卖出5号票
    thread_c号窗口卖出6号票
    thread_b号窗口卖出4号票
    thread_c号窗口卖出4号票
    thread_a号窗口卖出3号票
    thread_c号窗口卖出2号票
    thread_b号窗口卖出2号票
    thread_a号窗口卖出1号票
    thread_c号窗口卖出0号票
    thread_a号窗口卖出-1号票
    复制代码

      我们可以看到4号票和2号票被卖出了2次。还出现了0次和-1号票,原因是因为线程b在卖出4票的时候,因为线程c也同时在操作4号票,由于b操作有1秒钟等待,在这个过程中如果c读取到了票的数量,这时候 c读到的数据也是4。所以会出现同一张票卖出了2次。0和-1也是同样的道理。

      出现了上述情况怎样改变呢,我们可以这样做:当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后在把锁给另一个要用这个资源的线程。这样就不会出现上述情况。 实现这个锁的功能就需要用到synchronized这个关键字。

      

      1、使用同步方法:

      修改run方法,为run方法添加synchronized关键字:

    复制代码
    public synchronized void run() {
        for (int i = 1; i < 50; i++) {
            // 休眠1s秒中,为了使效果更明显,否则可能出不了效果
            if (ticket > 0) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "号窗口卖出" + this.ticket-- + "号票");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    复制代码

      执行结果:

    复制代码
    thread_a号窗口卖出10号票
    thread_a号窗口卖出9号票
    thread_a号窗口卖出8号票
    thread_a号窗口卖出7号票
    thread_a号窗口卖出6号票
    thread_a号窗口卖出5号票
    thread_a号窗口卖出4号票
    thread_a号窗口卖出3号票
    thread_a号窗口卖出2号票
    thread_a号窗口卖出1号票
    复制代码

      这时发现虽然不会卖出同一种票,但是现在这里只有a线程在执行,其他线程没有执行。原因是因为a线程在执行时给我们资源加上了锁,除非等循环完毕后才会释放锁,所以b和c都无法执行。这时候我们将卖票的代码提取出来,单独写一个方法,然后为这个方法添加同步方法。修改之后的代码如下:

    复制代码
    public class TicketThread implements Runnable {
        // 票的总数
        private int ticket = 10;
    
        @Override
        public void run() {
            for (int i = 1; i < 50; i++) {
                // 休眠1s秒中,为了使效果更明显,否则可能出不了效果
                try {
                    Thread.sleep(1000);
                    sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        //提取的卖票方法,避免run方法被锁.
        public synchronized void sale() {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "号窗口卖出" + this.ticket-- + "号票");
            }
        }
    }
    复制代码

      执行结果:

    复制代码
    thread_a号窗口卖出10号票
    thread_b号窗口卖出9号票
    thread_c号窗口卖出8号票
    thread_c号窗口卖出7号票
    thread_b号窗口卖出6号票
    thread_a号窗口卖出5号票
    thread_c号窗口卖出4号票
    thread_a号窗口卖出3号票
    thread_b号窗口卖出2号票
    thread_a号窗口卖出1号票
    复制代码

      执行结果就正常了。这个就是同步方法。 如果使用同步代码块,刚才的代码就不需要提取方法了,直接在run方法中就可以实现

      

      2、同步代码块。

    复制代码
    public class TicketThread implements Runnable {
        // 票的总数
        private int ticket = 10;
    
        @Override
        public void run() {
            for (int i = 1; i < 50; i++) {
                // 休眠1s秒中,为了使效果更明显,否则可能出不了效果
                try {
                    Thread.sleep(1000);
                    synchronized (this) {    //同步代码块
                        if (ticket > 0) {
                            System.out.println(Thread.currentThread().getName() + "号窗口卖出" + this.ticket-- + "号票");
                        }
                    }                
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    复制代码

      同步代码块实现方式与同步方法一致,但是更加灵活。

      

    八、线程间的通信

      线程间的相互作用:线程之间需要一些协调通信,来共同完成一件任务。

      Object类中相关的方法有两个notify方法和三个wait方法:

      http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

      因为wait和notify方法定义在Object类中,因此会被所有的类所继承。

      这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。

      

      1、wait()方法
      wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。

      当前的线程必须拥有当前对象的monitor,也即lock,就是锁。

      线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。

      要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。

      wait方法和sleep方法的区别:

      当线程调用了wait()方法时,它会释放掉对象的锁。

      另一个会导致线程暂停的方法:Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。


      2、notify()方法
      notify()方法会唤醒一个等待当前对象的锁的线程。

      如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait方法中的一个)。

      被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。

      被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。也就是说,被唤醒的线程并没有什么优先权,也没有什么劣势,对象的下一个线程还是需要通过一般性的竞争。

      notify()方法应该是被拥有对象的锁的线程所调用。

      (This method should only be called by a thread that is the owner of this object's monitor.)

      换句话说,和wait()方法一样,notify方法调用必须放在synchronized方法或synchronized块中。

      wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中。

      一个线程变为一个对象的锁的拥有者是通过下列三种方法:

      1.执行这个对象的synchronized实例方法。

      2.执行这个对象的synchronized语句块。这个语句块锁的是这个对象。

      3.对于Class类的对象,执行那个类的synchronized、static方法。

      

    程序实例 
      利用两个线程,对一个整形成员变量进行变化,一个对其增加,一个对其减少,利用线程间的通信,实现该整形变量0101这样交替的变更。

    复制代码
    NumberTest
    
    public class NumberHolder
    {
        private int number;
    
        public synchronized void increase()
        {
            if (0 != number)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
    
            // 能执行到这里说明已经被唤醒
            // 并且number为0
            number++;
            System.out.println(number);
    
            // 通知在等待的线程
            notify();
        }
    
        public synchronized void decrease()
        {
            if (0 == number)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
    
            }
    
            // 能执行到这里说明已经被唤醒
            // 并且number不为0
            number--;
            System.out.println(number);
            notify();
        }
    
    }
    
    
    
    public class IncreaseThread extends Thread
    {
        private NumberHolder numberHolder;
    
        public IncreaseThread(NumberHolder numberHolder)
        {
            this.numberHolder = numberHolder;
        }
    
        @Override
        public void run()
        {
            for (int i = 0; i < 20; ++i)
            {
                // 进行一定的延时
                try
                {
                    Thread.sleep((long) Math.random() * 1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
    
                // 进行增加操作
                numberHolder.increase();
            }
        }
    
    }
    
    
    
    public class DecreaseThread extends Thread
    {
        private NumberHolder numberHolder;
    
        public DecreaseThread(NumberHolder numberHolder)
        {
            this.numberHolder = numberHolder;
        }
    
        @Override
        public void run()
        {
            for (int i = 0; i < 20; ++i)
            {
                // 进行一定的延时
                try
                {
                    Thread.sleep((long) Math.random() * 1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
    
                // 进行减少操作
                numberHolder.decrease();
            }
        }
    
    }
    
    
    
    public class NumberTest
    {
        public static void main(String[] args)
        {
            NumberHolder numberHolder = new NumberHolder();
            
            Thread t1 = new IncreaseThread(numberHolder);
            Thread t2 = new DecreaseThread(numberHolder);
                    
            t1.start();
            t2.start();
        }
    
    }
    复制代码

      

      

      如果再多加上两个线程呢?

      即把其中的NumberTest类改为如下:

      

    复制代码
    public class NumberTest
    {
        public static void main(String[] args)
        {
            NumberHolder numberHolder = new NumberHolder();
            
            Thread t1 = new IncreaseThread(numberHolder);
            Thread t2 = new DecreaseThread(numberHolder);
            
            Thread t3 = new IncreaseThread(numberHolder);
            Thread t4 = new DecreaseThread(numberHolder);
                    
            t1.start();
            t2.start();
            
            t3.start();
            t4.start();
        }
    
    }
    复制代码

      运行后发现,加上t3和t4之后结果就错了。

      为什么两个线程的时候执行结果正确而四个线程的时候就不对了呢?

      因为线程在wait()的时候,接收到其他线程的通知,即往下执行,不再进行判断。两个线程的情况下,唤醒的肯定是另一个线程;但是在多个线程的情况下,执行结果就会混乱无序。

      比如,一个可能的情况是,一个增加线程执行的时候,其他三个线程都在wait,这时候第一个线程调用了notify()方法,其他线程都将被唤醒,然后执行各自的增加或减少方法。

      解决的方法就是:在被唤醒之后仍然进行条件判断,去检查要改的数字是否满足条件,如果不满足条件就继续睡眠。把两个方法中的if改为while即可。

    复制代码
    NumberHolder 4线程
    
    public class NumberHolder
    {
        private int number;
    
        public synchronized void increase()
        {
            while (0 != number)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
    
            // 能执行到这里说明已经被唤醒
            // 并且number为0
            number++;
            System.out.println(number);
    
            // 通知在等待的线程
            notify();
        }
    
        public synchronized void decrease()
        {
            while (0 == number)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
    
            }
    
            // 能执行到这里说明已经被唤醒
            // 并且number不为0
            number--;
            System.out.println(number);
            notify();
        }
    
    }
    复制代码
  • 相关阅读:
    一些Cassandra+YCSB异常
    memcached使用
    YCSB报": No such file or directory"异常
    dynamo与cassandra区别
    XT535
    北京地区护照办理流程
    一些iptables配置
    debian6保存iptables规则
    pdf转eps后存在大片空白的处理
    sql server 2008 数据库可疑的解决步骤
  • 原文地址:https://www.cnblogs.com/futao123/p/5068635.html
Copyright © 2020-2023  润新知