陌路lui  

  线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

一、进程与线程

1、进程

  进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位,它是程序执行时的一个实例。进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。一般而言,进程包含三个特征:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程拥有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响

2、线程

  线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,每个线程有自己的堆栈和局部变量,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

  线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须拥有一个父进程,因为多个线程可以共享父进程的全部资源,所以必须更加小心,确保线程不会妨碍到同一进程里的其他线程。线程是独立运行的,它并不知道进程中还有其他线程存在。线程的执行时抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。

3、进程与线程的联系

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,因此线程可以读写同样的数据结构和变量,便于线程之间的通信,但拥有自己的栈空间,拥有独立的执行序列。

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。
  • 处理机分给线程,即真正在处理机上运行的是线程。
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换

4、进程和线程的区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程

  1. 线程的划分尺度小于进程,使得多线程程序的并发性高。
  2. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  3. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  4. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

二、线程的状态

 1、新建(NEW):新创建了一个线程对象。

2、可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

3、运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

4、阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种: 

  1.  等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
  2. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
  3.  其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

5、死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

 

 

 

三、线程的常用方法

 Thread.currentThead():获取当前线程对象

 getPriority():获取当前线程的优先级

 setPriority():设置当前线程的优先级,注意:线程优先级高,被CPU调度的概率大,但不代表一定会运行,还有小概率运行优先级低的线程。

 isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)

 join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。有可能被外界中断产生InterruptedException 中断异常。

 sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。

 yield():调用yield方法的线程,暂停当前方法,释放自己拥有的CPU,线程进入就绪状态,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)

 interrupt():中断线程,由运行状态到死亡状态,中断线程操作实质上是修改了一下中断标示位为true,当前线程正在阻塞,修改标识位,如果是join,sleep,yield,则会抛出Interrup异常,修改标示位为false。

 wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的

 notify():唤醒当前线程,进入运行状态。该方法要在同步方法或者同步代码块中才使用的

 notifyAll():唤醒所有等待的线程。该方法要在同步方法或者同步代码块中才使用的

start()和run():start() 启动线程并执行相应的run()方法, run() 子线程要执行的代码放入run()方法,我们会发现,调用start()方法才是 启动一个新的线程。调用run()方法只是主线程调用了一个类的一个普通方法。

getName()和setName():getName() 获取此线程的名字,setName() 设置此线程的名字。

四、多线程

串行:相对于单条线程来执行多个任务来说的,按照顺序依次执行这些任务,完成一个任务之后就执行下一个任务

并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

并发:并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。

多线程编程的优点:

  • 进程之间不能共享内存,但线程之间共享内存非常容易
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
  • Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程

线程安全问题

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。

案例:需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。

class ThreadTrain1 implements Runnable{
    private int count = 100;
    private static Object oj = new Object();
    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(50);
            } catch (Exception e) {
                // TODO: handle exception
            }
            sale();
        }
    }

    public void sale() {
        if (count > 0) {
            System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
            count--;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        ThreadTrain1 threadTrain1 = new ThreadTrain1();
        Thread t1 = new Thread(threadTrain1, "①号窗口");
        Thread t2 = new Thread(threadTrain1, "②号窗口");
        t1.start();
        t2.start();
    }
}
View Code

 

 

 

结论发现,多个线程共享同一个全局成员变量时,做写的操作可能会发生数据冲突问题。

线程安全解决办法

使用多线程之间同步synchronized或使用锁(lock),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

同步代码块

就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(同一个数据){  //这个对象
可能会发生线程冲突问题
}
这就是同步代码块,对象如同锁,持有锁的线程可以在同步中执行 ,没持有锁的线程即使获取CPU的执行权,也进不去 。

  • 必须要有两个或者两个以上的线程
  • 必须是多个线程使用同一个锁
  • 必须保证同步中只能有一个线程在运行

好处:解决了多线程的安全问题;弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。

public void sale() {
        synchronized (oj) {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                count--;
            }
        }
    }

 

 

同步函数

首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

public synchronized void sale() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                count--;
            }

    }

静态同步函数

静态同步函数:方法上加上static关键字,使用synchronized 关键字修饰 或者使用类.class文件。
静态的同步函数使用的锁为该函数所属字节码文件对象,可以用 getClass方法获取,也可以用当前 类名.class 表示。
    public static void sale() {
        synchronized (ThreadTrain1.class) {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                count--;
            }
        }
    }
面试常问:
一个线程使用同步函数,另一个线程使用同步代码块(this),能够实现同步。
一个线程使用同步函数,另一个线程使用同步代码块(非this),不能实现同步。
一个线程使用同步函数,另一个线程使用静态同步函数,不能实现同步。
总结:
同步函数使用this锁;
同步代码块可使用任意对象锁或者this锁;
静态同步函数使用类的字节码.class文件锁。

Lock锁

在需要的时候我们需要手动去获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。

    public  void sale() {
        lock.lock(); // 获取锁对象
        try{
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                count--;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock(); // 释放锁对象
        }
    }

Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

ryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

 public void sale() throws InterruptedException{
//        lock.lock(); // 获取锁对象
        // 如果2秒内获取不到锁对象,那就不再等待
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                    count--;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 释放锁对象
            }
        }
    }

死锁

  当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。

public class Test  implements Runnable{

        A a = new A();
        B b = new B();
        public void init(){
            Thread.currentThread().setName("主线程");
            a.foo(b);
            System.out.println("进入了主线程之后");
        }
        @Override
        public void run() {
            Thread.currentThread().setName("副线程");
            b.bar(a);
            System.out.println("进入了副线程之后");
        }


    public static void main(String[] args)  {
        Test deadLock = new Test();
        new Thread(deadLock).start();
        deadLock.init();
    }
}
class A{
    public synchronized void foo(B b){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了A实例的foo()方法");//1
        try{
            Thread.sleep(200);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"调用B实例的last()方法");//3
        b.last();
    }
    public synchronized  void last(){
        System.out.println("进入了A类的last()方法内部");
    }
}

class B{
    public synchronized void bar(A a){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了B实例的bar()方法");//2
        try{
            Thread.sleep(200);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"调用A实例的last方法");//4
        a.last();
    }
    public synchronized void last(){
        System.out.println("进入了B类的last()方法内部");
    }
}

 

 

五、多线程三大特性

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题: 比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

有序性

程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

 

posted on 2020-07-21 11:38  陌路lui  阅读(19)  评论(0编辑  收藏  举报