线程

参考资料:郝斌老师的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
 
 
 
========== ======== ======= ====== ====== ===== ==== === == =
 
 
 
 
原文地址:https://www.cnblogs.com/asdfknjhu/p/14588415.html