• 线程


    参考资料:郝斌老师的Java学习视频
     
    一,进程( process )与程序( program )
    1,定义:
    程序:只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体
    进程:是程序在某个数据集上的执行。
    进程是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。
     
    2,进程的由来:
    通俗点说,为了不破坏“程序”这个词原有的含义:“是一个在时间上严格有序的指令集合,是静态的保存在存储介质上。”而又能刻画多个程序共同运行时呈现出的新特征,所以引入了进程这一概念。
    如:记事本是一个程序,打开的记事本是运行的程序,但在电脑中我们同时打开多个记事本,此时程序与运行的程序无法对应,所以就需要引入进程的概念。即一个记事本程序打开了多个记事本进程。
     
     
    二,线程( thread )
    1,定义:
    线程:一个程序里的不同执行路径。
     
    2,解释
    以前所编写的程序,每个程序都有一个入口,一个出口以及一个顺序执行的序列,在程序执行过程中的任何指定时刻,都只有一个单独的执行点。
    事实上,在单个程序内部是可以在同一时刻进行多种运算的,即所谓的多线程。
    另外:ctrl + c 可以停止死循环(cmd 中的)
     
    3,例子
    class A extends Thread
    {
        public void run()
        {
            while(true)
            {
                System.out.println("AAA");
            }
        }
    }
    public class M
    {
        public static void main(String[] args) 
        {
            A aa = new A();
            // aa.start(); // 会开辟一个新的线程,并自动调用run方法,如果类中没有run方法(注意方法名字只能run),则不会调用 aa 的任何方法
            aa.start(); 
            // aa.run();  // 单线程执行,直接调用 run() 是不会创建一个新的线程
            while(true)
            {        
                System.out.println("BBB");
            }
        }
    }
    View Code
     
    4,多线程的优势:
    多线程编程简单,效率高(能直接共享数据和资源,多进程不能)
    适合于开发服务程序(如服务,聊天服务等)
     
    5,创建一个线程的第一种方法
    ① 创建一个继承Thread的类(假定类名为A),并重写Thread中的run方法
    ② 构造一个A类对象,假定对象名为aa
    ③ 调用aa的start方法(start方法是从Thread继承过来的)
    注意:
    调用start() 方法,会开辟一个新的线程,并自动调用run方法,如果类中没有run方法(注意方法名字只能run),则不会调用 aa 的任何方法
    一个Thread对象能且只能代表一个线程。
    一个Thread对象不能调用两次start()方法,否则会抛出java.lang.IllegalThreadStateException异常
     
    6,线程与CPU
    执行完aa. start();后并不表示aa所对应的线程就一定会立即得到了执行,
    aa.start();执行完后 只是表示aa线程具有了可以立即被CPU执行的资格,
    但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行aa所对应的线程。
    所以 AAA 和 BBB 不一定哪个先输出。
    CPU:优先级,执行时间,等待时间,紧急程度
     
    7,多线程的切换
    8,线程的生命周期 
    ① 创建状态 
      当用new操作符创建一个新的线程对象时,该线程处于创建状态。
      处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。
      要通过调用start方法启动该线程。
    ② 可运行状态 
      执行线程的start()方法将为线程分配必须的系统资源,安排其运行,并调用线程体中的run()方法,这样就使得该线程处于可运行( Runnable )状态。
      这一状态并不是运行中状态(Running ),因为线程还要在cpu排队。
      返回可运行状态的原因:
        处于睡眠状态的线程在指定的时间过去后; 
        如果线程在等待某一条件,另一个对象必须通过notify()或notifyAll()方法通知等待进程条件的改变; 
        如果线程是因为输入/输出阻塞,输入/输出完成 
    ③ 不可运行状态 
      当发生下列事件时,处于运行状态的线程会转入到不可运行状态:
        调用了sleep()方法;
        线程调用wait方法所等待的特定条件的满足;
        线程输入/输出阻塞。 
    ④ 消亡状态
      当线程的run方法执行结束后,该线程自然消亡。
    9,创建一个新线程的第二种方法
    ① 定义一个实现了Runnable接口的类,假定为A
    ② 创建A类对象aa,代码如下
        A aa = new A();
    ③ 利用aa构造一个Thread对象tt,
        Thread tt = new Thread(aa);
    ④ 调用t中的start方法
        tt.start();
    其中:Thread的构造方法为:public Thread(Runnable target)
    class A implements Runnable
    {
        public void run()
        {
            while(true)
            {
                System.out.println("AAA");
            }
        }
    }
    public class M
    {
        public static void main(String[] args) 
        {
            A aa = new A();
            Thread t = new Thread(aa); //Thread的构造方法 public Thread(Runnable target)
            t.start();
            while(true)
            {        
                System.out.println("BBB");
            }
        }
    }
    View Code
    10,Thread 的常用方法
    public final void setName(String name)
        设置当前线程的名字
    public static Thread currentThread()
        返回对当前正在执行的线程对象的引用.
    public final String getName()
        返回当前线程的名字
    class A extends Thread
    {
        public void run()
        {    
            System.out.printf("%s在执行!
    ",Thread.currentThread().getName());
        }
    }
    public class M
    {
        public static void main(String[] args) 
        {
            A aa1 = new A();
            aa1.setName("张三");
            aa1.start();
            A aa2 = new A();
            aa2.setName("李四");
            aa2.start();
            A aa3 = new A();
            aa3.setName("王五");
            aa3.start();
            System.out.printf("%s在执行!
    ",Thread.currentThread().getName());
        }
    }
    View Code
     
     
     三,线程控制
    1,线程优先级
    Java提供 一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。线程的优先级用数字表示范围从1到10。主线程的缺省优先级是5,子线程的优先级默认与其父线程相同。通常高优先级的线程将先于优先级低的线程执行,但并不总是这样,因此实际开发中并不单纯依赖优先级来决定线程运行次序。
            Thread.NORM_PRIORITY = 5
            Thread.MAX_PRIORITY = 10
            Thread.MIN_PRIORITY = 1
    可以使用下述线方法获得或设置线程对象的优先级:
    int getPriority();
    void setPriority(int newPriority);
    class T1 implements Runnable 
    {
        public void run() {
            for(int i=0; i<100; i++) {
                System.out.println("T1: " + i);
            }
        }
    }
    
    class T2 implements Runnable 
    {
        public void run() {
            for(int i=0; i<100; i++) {
                System.out.println("------T2: " + i);
            }
        }
    }
    public class M 
    {
        public static void main(String[] args) 
        {
            Thread t1 = new Thread(new T1());
            Thread t2 = new Thread(new T2());
            t1.setPriority(Thread.NORM_PRIORITY + 5);  
            t1.start();
            t2.start();
        }
    }
    View Code

    2,线程的休眠.
    线程休眠:暂停执行当前运行中的线程,使之进入阻塞状态,待经过指定的“延迟时间"后再醒来并转入到就绪状态。
    Thread类提供的相关方法:
      public static void sleep(long millis) throws InterruptedException
      public static void sleep(long millis, int nanos) throws InterruptedException
    由于是静态方法,可以由Thread直接调用。
    class A implements Runnable
    {
        public void run()  // throws InterruptedException
        { 
            while(true)
            {
                System.out.println(Thread.currentThread().getName());
                int j = 100;
                while(j-- != 0)
                    System.out.println("hhhhhhhhhhhhhhhhh");
                try
                {
                     Thread.sleep(4000);   // 如果没有其他线程,则控制台只能干等               
                }
                catch (Exception e){}    
                /* Thread.sleep(4000);    
                重写方法抛出的异常范围不能大于被重写方法抛出的异常范围
                如果没有 try—catch 的话,直接在 run 方法抛出异常,throws InterruptedException
                因为 run 是重写方法,实现的接口 Runnable run() 方法,而接口的 run() 方法 是不抛出异常的
                所以会报错的
    
                综上,sleep 会抛出异常,而 run() 方法无法抛出异常,所以必须用 try—catch 语句捕捉异常
                 */
            }
        }
    }
    public class M
    {
        public static void main(String[] args)
        {
            A aa = new A();
            Thread tt = new Thread(aa);
            tt.start();    
    
            while(true)
            {
                System.out.println("哈哈");
            }
        }
    }
    View Code
    注意:重写方法抛出的异常范围不能大于被重写方法抛出的异常范围。
    如果直接在 run 方法抛出异常,throws InterruptedException。因为 run 是重写方法,实现的是Runnable接口的run() 方法,而Runnable的 run() 方法 是不抛出异常的,所以会报错的。Thread 中的 run() 方法也是一样。
    简单来说,就是 sleep 会抛出异常,而 run() 方法无法抛出异常,所以必须用 try—catch 语句捕捉异常。

    3,线程的让步
    让运行中的线程主动放弃当前获得的CPU处理机会,但不是使该线程阻塞,而是使之转入就绪状态。所以如果没有优先级更高或相等的线程,让步就相当什么也没做,
      public static void yield(
    class MyThread implements Runnable 
    {
        public void run()
        {
            for(int i = 1 ; i <= 100 ; i++)
            {
                System.out.println(Thread.currentThread().getName()+": " + i);
                  if(0 == i % 10)
                  {
                    Thread.yield();
                  }
            }
          }
    }
    public class M
    {
        public static void main(String[] args) 
        {
           MyThread mt = new MyThread();
           Thread t1 = new Thread(mt);
           Thread t2 = new Thread(mt);
           
           t1.setName("线程A");
           t2.setName("线程B");
           
           t1.start();
           t2.start();
        }
    }
    View Code
    4,线程的串行化
    在多线程程序中,如果在一个线程运行的过程中要用到另一个线程的运行结果,则可进行线程的串型化处理。
      public final void join() throws InterruptedException
    暂停当前正在执行 t.join();的线程,直到t所对应的线程运行终止之后,暂停的线程才会获得继续执行的机会。
    class MyRunner implements Runnable 
    {
        public void run() 
        {
            for(int i = 0 ; i < 50 ; i++) {
                System.out.println("子线程: " + i);
            }
        }
    }
    public class M
    {    
        public static void main(String args[])
        {
            MyRunner r = new MyRunner();
            Thread t = new Thread(r);
            t.start(); // 开启子线程
            try
            {
                t.join(); 
                // 暂停当前正在执行 t.join();的线程(main 函数)
                // 直到t所对应的线程运行终止之后,当前线程(mian 函数)才会获得继续执行的机会
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
            for(int i = 0 ; i < 50 ; i++) 
            {
                System.out.println("主线程:" + i);
            }
        }
    }
    View Code

    四,线程的同步

    1,需要线程同步的原因——买票问题

    if ( 票数 > 0 )  {

      买一张票

      票数--

    }

    此时如果有三个网站在买票,当你在 A 网站买了一张票,但票数还没有减一的情况下,线程跳到了 B 网站,这样就导致了混乱,且到了最后一张票,还可能发生多人都买了,这样就导致最终票数被买到负数,而不是 0。

    所以,就需要将整个 if 当成整体,让 cpu 无法在 任意一个点切换,必须整体执行完才能切换,即线程的同步。

    // 一张票卖了两次
    class A implements Runnable
    {
        private static int tickets = 100;
            
        public void run()
        {
            while (true)
            {
                if (tickets > 0)
                {
                    System.out.printf("%s线程 正在卖出第% d 张票
    ", Thread.currentThread().getName(), tickets);
                    --tickets;    
                }
                else
                {
                    break;
                }
            }
        }
    }
    public class M
    {
        public static void main(String[] args)
        {
            A aa1 = new A();
            Thread t1 = new Thread(aa1);
            t1.start();    
            
            // 这样会导致一张票卖了两次
            A aa2 = new A();
            Thread t2 = new Thread(aa2);
            t2.start();        
        }
    }
    View Code
    2,synchronized  关键字
    synchronized 可以修饰:一个方法或者一个方法内部的某个代码块
    格式:
    synchronized(类对象名 aa)
    {
      同步代码块
    }
    synchronized(类对象名aa)的含义是:
    判断aa是否已经被其他线程霸占,如果发现已经被其他线程霸占,则当前线程陷入等待中。
    如果发现aa没有被其他线程霸占,则当前线程霸占住aa对象,并执行内部的同步代码块,在当前线程执行synchronized同步代码块时,其他线程将无法再执行该代码(因为当前线程已经霸占了aa对象)。
    若当前线程执行完其同步代码块,会自动释放对AA对象的霸占。
    此时其他线程会相互竞争对AA的霸占,最终CPU会选择其中的某个线程执行。
    最终导致的结果是一个线程正在操作某资源的时候,将不允许其它线程操作该资源,即一次只允许一个线程处理该资源。
     
    注: 
    霸占的专业术语叫锁定,霸占住的那个对象专业术语叫做监听器
    synchronized修饰一个方法时,实际霸占的是该方法的this指针所指向的对象,即正在调用该方法的对象。
    // 卖票
    class A implements Runnable
    {
        private static int tickets = 100;  //  tickets 要用 static 不然各买各的,就有200张票
        private static String str = new String("切不了我了吧"); // str 要用 static 修饰,保证两个线程锁定的是同一个资源
        
        public void run()
        {
            while (true)
            {
                synchronized (str)
                {
                    if (tickets > 0)
                    {
                        System.out.printf("%s 线程 正在卖出第 %d 张票
    ", Thread.currentThread().getName(), tickets);
                        --tickets;    
                    }
                    else
                        break;
                }
            }
        }
    }
    public class M
    {
        public static void main(String[] args)
        {
            A aa = new A();
            Thread t1 = new Thread(aa);
            t1.start();    
            
            Thread t2 = new Thread(aa);
            t2.start();        
        }
    }
    View Code

    3,生产与消费问题

    通常,一些同时运行的线程需要共享数据。在这种时候,每个线程就必须要考虑与其他一起共享数据的线程的状态与行为(同步),否则的话就不能保证共享数据的一致性,从而也就不能保证程序的正确性。
    与买票问题类似,在生产与消费问题,要用到栈。在用数组模拟栈时,元素的出入栈与栈顶指针的移动必须同步。
    错误的设计:无法保证生产先于消费运行
    // 不合理的消费和生产设计——无法保证生产先于消费运行
    class SynStack  
    {
        private int top = 0 ;
        private char data[] = new char[110];
        public void push(char ch) // 生产,入栈
        {
            data[top] = ch;
            top++;
        }
        public char pop()  // 消费,出栈
        {
            top--;
            return data[top];
        }
    }
    class Producer implements Runnable    // 生产者
    {
        private SynStack ss = null;
        public Producer(SynStack ss)
        {
            this.ss = ss;
        }
        public void run()
        {
            ss.push('a');
        }
    }
    class Consumer implements Runnable  // 消费者
    {
        private SynStack ss = null;
        public Consumer(SynStack ss)
        {
            this.ss = ss;
        }
    
        public void run()
        {
            System.out.printf("%c
    ",ss.pop());
        }
    }
    public class M
    {
        public static void main(String[] args)
        {
            SynStack ss = new SynStack();
            Producer p = new Producer(ss);
            Consumer c = new  Consumer(ss);
    
            Thread t1 = new Thread(p);
            t1.start();
    
            Thread t2 = new Thread(c);
            t2.start();
        }
    }
    View Code
    aa.wait()
        将执行aa.wait()的当前线程转入阻塞状态,让出CPU的控制权,并释放对aa的锁定
    aa.notify()
        假设执行aa. notify()的当前线程为T1。如果当前时刻有其他线程因为执行了aa.wait()而陷入阻塞状态,则叫醒其中一个,但具体是叫醒哪一个,这是由系统调度器控制,程序员无法控制。
        执行aa.notify() 方法时如果一个线程都没有叫醒的话,也是可以的。
        所谓叫醒某个线程就是令该线程从因为wait而陷入阻塞的状态转入就绪状态
    aa.notifyAll()
        叫醒所有因为执行了aa.wait()而陷入阻塞状态的线程
    正确的设计:
    // 生产消费 正确的设计
    class SynStack  
    {
        private int top = 0 ; // 栈顶指针
        private char st[] = new char[20];
        public synchronized void push(char ch) // 生产
        {
            /**
             *  如果空间满了,无法再生产的话,就将生产的方法wait()掉。
             *  显然,虽然下面有 notify() 但当前线程已经 wait() 掉了,就无法执行后面的语句,所以自己是唤醒不了自己的
             *  只能通过该对象的另外一个方法:消费,使用notify()来唤醒。
             *  这正对应无法生产下去的情况下,只能先消费一部分,再来生产。
             */
            /**
             * 如果只有一个生产和消费者:
             * 这里因为只有生产和消费相互影响,所以可以用 if
             * 因为如果 wait 之后被唤醒,一定是消费过了,
             * 所以就可以继续生产了
             * 但如果是有三个线程相互影响,则要用 while
             * 即唤醒之后,要再度循环,判断是因为什么而被唤醒的
             * 
             * 但是如果有多个消费者和生产者:
             * 也是要用while。
             * 因为生产者_1可能被生产者_2唤醒,而此时还没有消费,
             * 所以此时还是没有空间方产品
             * 一生产就数组越界了
             * 
             * 所以最好还是用 while 保险
             */
            while(top == st.length) 
            {
                try{
                    this.wait();
                }    
                catch(Exception e){
                }
            }
            this.notify(); // 如果生产没有唤醒的话,消费完了,就会陷入一直的等待生产了
    
            st[top] = ch;
            top++;
            System.out.printf("%s正在生产第 %d 个产品,该产品是: %c
    ",Thread.currentThread().getName(), top, ch);
        }
        public synchronized char pop()  // 消费
        {
            /**
             *  如果没有产品了,无法再消费的话,就将消费的方法wait()掉。
             *  显然,虽然下面有 notify() 但当前线程已经 wait() 掉了,就无法执行后面的语句,所以自己是唤醒不了自己的
             *  只能通过该对象的另外一个方法:生产,使用notify()来唤醒。
             *  这正对应在没有产品的情况下,只能等生产出来产品,才能消费。
             */
            /**
             * 如果只有一个生产和消费者:
             * 这里因为只有生产和消费相互影响,所以可以用 if
             * 因为如果 wait 之后被唤醒,一定是产生过了,
             * 所以就可以继续消费了
             * 但如果是有三个线程相互影响,则要用 while
             * 即唤醒之后,要再度循环,判断是因为什么而被唤醒的
             * 
             * 但是如果有多个消费者和生产者:
             * 也是要用while。
             * 因为消费者_1可能被消费者_2唤醒,而此时还没有生产,
             * 所以此时还是没有产品
             * 一消费就数组越界了
             * 
             * 所以最好还是用 while 保险
             */
            while(top == 0)
            {
                try{
                    this.wait();
                }    
                catch(Exception e){            
                }
            }
            this.notify(); // 如果消费没有唤醒的话,生产结束了,就会陷入一直的等待消费了
    
            top--;
            System.out.printf("%s正在消费第 %d 个产品,该产品是: %c
    ", Thread.currentThread().getName(), top+1, st[top]);
            return st[top];
        }
    }
    class Producer implements Runnable    // 生产者
    {
        private SynStack ss = null;
        public Producer(SynStack ss)
        {
            this.ss = ss;
        }
        public void run()    
        {
            for(int i = 0 ; i < 50; i++) // 生产 50 个字母
            {
                char ch = (char) ('a' + i%26);
                ss.push(ch);
            }
        }
    }
    class Consumer implements Runnable  // 消费者
    {
        private SynStack ss = null;
        public Consumer(SynStack ss)
        {
            this.ss = ss;
        }
        public void run()
        {
            for(int i = 0 ; i < 50; i++) // 消费 50 个字母
            {
                ss.pop();
            }
        }
    }
    public class M
    {
        public static void main(String[] args)
        {
            // 必须保证执行生产方法和消费方法的对象,是同一个对象
            SynStack ss = new SynStack();
            Producer p = new Producer(ss);
            Consumer c = new  Consumer(ss);
    
            Thread p1 = new Thread(p); // 生产者1
            p1.setName("生产者_1");
            Thread p2 = new Thread(p); // 生产者2
            p2.setName("生产者_2");
            Thread c1 = new Thread(c); // 消费者1
            c1.setName("消费者_1");
            Thread c2 = new Thread(c); // 消费者2
            c2.setName("消费者_2");
            p1.start();
            p2.start();
            c1.start();
            c2.start();
        }
    }
    View Code
     
     
     
    ========== ======== ======= ====== ====== ===== ==== === == =
     
     
     
     
  • 相关阅读:
    SqlServer2008 安装经验日志总结
    Moile手机开发日志总结若干问题
    安装和卸载Android应用程序(apk包)
    Android Intent 常见用法总结
    vs2008+sqlserver2008 同一台服务器webconfig 数据连接串的配置要点
    注意了android日期控件月份比实际少一个月
    Android 蓝牙开发研究
    [javascript] 怎样在javascript里面调试object
    [Ubuntu] 转载:ubuntu apache2配置
    [Ubuntu] 如何在Ubuntu11.04将PHP5.3降级到PHP5.2
  • 原文地址:https://www.cnblogs.com/asdfknjhu/p/14588415.html
Copyright © 2020-2023  润新知