线程与进程

2018-09-18 06:36:37来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

什么是进程:

    所谓进程(process)就是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。

 

什么是线程:

    一个线程是进程的一个顺序执行流。同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。线程是程序执行的最小程序单元。

进程与线程的区别 :

    一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存。线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个执行控制。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。

什么时候使用到线程:

    线程通常用于在一个程序中需要同时完成多个任务的情况。例如在游戏中我们会听到某些背景音乐,某个角色在移动,出现某些绚丽的动画效果等。多个线程在一起执行,进而就是我们说的多线程。

线程的优先级:

    每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。 

    线程的API 中展示的调用setproperty方法来设置线程的优先级:

    线程优先级默认值 5,从1到10逐渐提高 .

线程的生命周期:

  上面的图可以看出,线程的大概生命周期同时对于几种状态,哪几种呢?

   

  1. 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t =new MyThread();
  2. 就绪状态(Runnable):当调用线程对象的start()方法线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start()此线程立即就会执行。就绪状态是线程进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

  根据线程阻塞产生的原因不同,我们又可以将阻塞的状态分为三种:

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态
  2. 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态
  3. 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

 


  

说了那么多 ,那 我们如何产生一个线程呢?

        线程的产生有两种方法:

   1.  继承线程 Thread 类 

public class ThreadB extends Thread{
@Override
    public void run() {
    while(true){
    System.out.println("this is B");
    try {
        Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
            }
        }
    }
}                

     2 . 实现Runnable 接口 

 1 public class ThreadB implements Runnable{
 2     @Override
 3     public void run() {
 4     while(true){
 5         System.out.println("this is B");
 6 try {
 7         Thread.sleep(100);
 8 } catch (InterruptedException e) {
 9         e.printStackTrace();
10             }
11            }
12     }
13 }                            

线程的开启方式:

public static void main(String[] args){   
//继承Thread类的方法,直接开启线程 new ThreadA().start(); //实现Runnable的方法。new 一个线程类对象,传入new 的线程对象 再开启线程
通过Thread类创建线程,并将实现了Runnable接口的子类对象作为参数传递给Thread类的构造函数。
  new Thread(new ThreadA()).start(); }

    这里要注意了:  因为 Java 是单继承的,在我们日常开发中,一般会选择实现Runnable 接口的方式去开启线程, 避免无法继承其他类的情况。

  线程安全问题:

  多个线程访问出现的延迟和线程的随机性,会导致我们线程出现一系列问题,在理想情况下,线程安全问题不会存在,不容易出现,但是一旦出现,对我们软件的影响是非常大的。当多个线程并发读写同一个临界资源比如 多线程共享的实例变量,以及多线程共享的静态公共变量等 的时候会发生“线程并发安全问题”,由此我们产生了同步(synchronized
操作。

    同步操作有几种,介绍几种常用的方法:

     1 . 同步方法

     2.  同步代码块

     3.  重入锁 

 同步的前提是 :  同步需要两个或两个以上的线程,多个线程使用的是同一个锁。 不满足这两个,就不能称为同步。

 当然同步也不是万能的 ,它也有弊端: 比如当线程非常多的时候,因为每个线程都会去判断同步上的锁,这是很耗费资源的, 无形中会降低程序的执行效率。

      Synchronized 用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时释放锁。

       同步方法:synchronized 加在方法上面。

 

1 public synchronized 返回值类型 方法名(参数列表){
2              // 逻辑代码
3 }    

    同步代码块:synchronized 加在方法体之中,需要执行同步的代码块前面。

Synchronized(同步监视器){
        //需要进行同步的逻辑代码块
}

  重入锁 :

  Java中的锁框架指的是java.util.concurrent.locks这个包里的,不同于对象的内置加锁同步以及java.lang.Object的等待/通知机制,包含锁框架的并发工具通过轮询锁、限时等待及其他方式改善了这种机制。 这里的锁指的是接口Lock。

  类ReentranLock实现了接口Lock,这是一个可重入的互斥锁,这个锁是和一个持有量相关联的。当一条线程持有这个锁并且调用lock()、lockUnitinterruptibly()或者任意一个tryLock()方法重新获取锁,这个持有量就递增1。当线程调用unlock()方法,持有量就递减1。当持有量为0时,锁就会被释放。

1 Lock  lock = new ReentrantLock();// 实例化接口
2         lock.lock();
3         try {
4             //使用锁获取到的资源
5         } catch (Exception e) {
6             //异常处理
7         } finally {
8         lock.unlock();
9         }

   既然说到了锁 。我们就来谈谈 基本锁概念;

    内置锁:

  •   java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
  •   java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

   对象锁与类锁:  

java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,

  •   对象锁是用于对象实例方法,或者一个对象实例上的
  •   类锁是用于类的静态方法或者一个类的class对象上的。

  我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

   守护线程: 

  用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。

  守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。

  虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。

  与用户线程的不同点:

   当进程结束时,所有正在运行的守护线程都会被强制结束。而一个进程中所有前台线程都结束,进程就会结束

 另外有几点需要注意:

1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
2、在守护线程中产生的新线程也是守护线程。
3、不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

   试想既然我们一个进程有多个线程组成,当我们组成执行这个程序时, 多个进程协调执行,如果进程需要其他进程的执行结果或者参数怎么办呢?  这就涉及到我们进程间的通讯了。

   进程间的通讯有以下几种: 

  •         同步     即 多个线程通过synchronized关键字这种方式来实现线程间的通信。

    依旧 举个栗子:

 1 public class Demo {
 2 
 3     synchronized public void methodA() {
 4         //逻辑代码块
 5     }
 6 
 7     synchronized public void methodB() {
 8         //逻辑代码块
 9     }
10 }
11 
12 public class ThreadA extends Thread {
13 
14     private Demo demo;
15     //构造方法
16     @Override
17     public void run() {
18         super.run();
19         demo.methodA();
20     }
21 }
22 
23 public class ThreadB extends Thread {
24 
25     private Demo demo;
26     //构造方法
27     @Override
28     public void run() {
29         super.run();
30         demo.methodB();
31     }
32 }
33 
34 public class Run {
35     public static void main(String[] args) {
36         Demo demo = new Demo();
37 
38         //线程A与线程B 持有的是同一个对象:demo
39         ThreadA one = new ThreadA(object);
40         ThreadB two = new ThreadB(object);
41         one.start();
42         two.start();
43     }
44 }           

    线程A和线程B持有同一个Demo类的对象demo,这两个线程需要调用不同的方法,但是它们是同步执行的,所以:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了通信。

  这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(即对象,便获得了访问权限),谁就可以执行。

  •    while轮询的方式  不停地通过while语句询问条件是否成立 

   还是举个栗子:

public class Test {

    private List<String> list = new ArrayList<String>();

    public void add() {
        list.add("elements");
    }

    public int size() {
        return list.size();
    }
}

public class ThreadA extends Thread {
    
        private Test test;

        public ThreadA(Test test) {
            super();
            this.test = test;
        }
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    test.add();
                    System.out.println(ThreadA.currentThread()+ "添加了" + (i + 1) + "个元素");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
}

public class ThreadB extends Thread {
     private Test test;

        public ThreadB(Test test) {
            super();
            this.test = test;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    if (test.size() == 5) {
                        System.out.println("==5, 线程b准备退出了");
                        throw new InterruptedException();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
 }


public class Run{
    
      public static void main(String[] args) {
            Test service = new Test();

            ThreadA a = new ThreadA(service);
            a.setName("A");
            a.start();

            ThreadB b = new ThreadB(service);
            b.setName("B");
            b.start();
        }

}

线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。

  这种方式还存在另外一个问题:

轮询的条件的可见性问题,关于内存可见性问题。线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的 条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环。

  •   wait/notify机制 

import java.util.ArrayList;
import java.util.List;

public class MyList {

    private static List<String> list = new ArrayList<String>();

    public static void add() {
        list.add("anyString");
    }

    public static int size() {
        return list.size();
    }
}


public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("wait begin "
                            + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("wait end  "
                            + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class ThreadB extends Thread {
    private Object lock;

    public ThreadB(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    MyList.add();
                    if (MyList.size() == 5) {
                        lock.notify();
                        System.out.println("已经发出了通知");
                    }
                    System.out.println("添加了" + (i + 1) + "个元素!");
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Run {

    public static void main(String[] args) {

        try {
            Object lock = new Object();

            ThreadA a = new ThreadA(lock);
            a.start();

            Thread.sleep(50);

            ThreadB b = new ThreadB(lock);
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

    线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。

     A,B之间如何通信的呢?

  也就是说,线程A如何知道 list.size() 已经为5了呢? 这里用到了Object类的 wait() 和 notify() 方法。 当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。---不像②while轮询那样占用CPU 当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。 这种方式的一个好处就是CPU的利用率提高了。 但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。

  •   管道通信就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

    具体就不介绍了。分布式系统中说的两种通信机制:共享内存机制和消息通信机制。感觉前面的①中的synchronized关键字和②中的while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现进程间的交流。 而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:[二十五]JavaIO之RandomAccessFile

下一篇:约束导入 --- Hibernate入门学习之常见设置三部曲之一