Java学习笔记(五): 线程

线程

并行和并发

并行:指两个或多个事件在同一时刻发生;
并发:指两个或读个事件在同一时间段内发生。

Concurrency is when two tasks can start, run, and complete in overlapping time periods. Parallelism is when tasks literally run at the same time, eg. on a multi-core processor.
Concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations.
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
An application can be concurrent – but not parallel, which means that it processes more than one task at the same time, but no two tasks are executing at same time instant.
An application can be parallel – but not concurrent, which means that it processes multiple sub-tasks of a task in multi-core CPU at same time.
An application can be neither parallel – nor concurrent, which means that it processes all tasks one at a time, sequentially.
An application can be both parallel – and concurrent, which means that it processes multiple tasks concurrently in multi-core CPU at same time.
Vipin Jain. Differences between concurrency vs. parallelism

翻译成中文:

并发是两个任务可以在重叠的时间段内启动,运行和完成。并行是任务在同一时间运行,例如,在多核处理器上。
并发是独立执行过程的组合,而并行是同时执行(可能相关的)计算。
并发是一次处理很多事情,并行是同时做很多事情。
应用程序可以是并发的,但不是并行的,这意味着它可以同时处理多个任务,但是没有两个任务在同一时刻执行。
应用程序可以是并行的,但不是并发的,这意味着它同时处理多核CPU中的任务的多个子任务。
一个应用程序可以即不是并行的,也不是并发的,这意味着它一次一个地处理所有任务。
应用程序可以即是并行的也是并发的,这意味着它同时在多核CPU中同时处理多个任务。


进程与线程

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个是线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间是可以影响的,又称为轻型进程或进程元

Java程序的进程(Java的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程)。

区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

优缺点

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

线程调度:

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
 在运行池中,会有多个处于就绪状态的线程在等待CPU,JVM的一项任务就是负责线程的调度,JVM采用的是抢占式调度,没有采用分时调度,因此可能会造成多线程执行结果的随机性。

多线程的优势

1.进程之间不能共享内存,而线程之间共享内存(堆内存);
2.系统创建进程时需要为该进程重新分配系统资源,创建线程则代价较小,因此实现多任务并发时,多线程效率更高;
3.Java语言本身内置多线程功能的支持,而不是单纯的作为底层系统的操作方式,从而简化了多线程编程;

Java操作进程(IO流)

1.Runtime类的exec方法

public static void main(String[] args) throws IOException {
    //方式一:使用Runtime类的exec方法
    Runtime runtime = Runtime.getRuntime();
    runtime.exec("notepad");    
}

2.ProcessBuilder类中的start方法:

public static void main(String[] args) throws IOException {
    //方式二:使用ProcessBuilder中的start方法
    ProcessBuilder pb = new ProcessBuilder("notepad");
    pb.start();
}

创建并启动线程

传统的有两种方式:

方式一:继承Thread类:
1.定义一个类A,继承与java.lang.Thread类;
2.在A类中覆盖Thread类中的run方法;
3.我们在run方法中编写需要执行的操作—->线程执行体;
4.在main方法(线程)中,创建线程对象,并启动线程;

创建线程类:                  Aa = new A类();
调用线程对象的start方法:      a.start();         //启动一个线程

注意:千万不要调用run方法,如果调用run方法,就好比对象调用方法,依然只有一个线程,并没有开始新的线程。start方法会在底层调用run方法。

//播放音乐线程类
class MusicThread extends java.lang.Thread {

    public void run() {
        for (int i = 1; i <= 50; ++i) {
            System.out.println("播放音乐" + i);
        }
    }
}

// 方式一:继承Thread类
public class ExtendsThreadDemo {

    public static void main(String[] args) {
        // 主线程:运行游戏
        for (int i = 1; i <= 50; ++i) {
            System.out.println("打游戏"+ i);
            if (i == 10){
                //创建线程对象,启动线程
                MusicThread t = new MusicThread();
                t.start();
            }
        }
    }
}

注意:线程只能启动一次,其结果有不可预知性

方式二:实现Runnable接口:
1.定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类
2.在A类中覆盖Runnable接口中的run方法
3.我们在run方法中编写需要执行的操作—->run方法里的,线程执行体
4.在main方法中,创建线程对象,并启动线程。

创建线程类对象:
Thread t = new Thread(new A());
调用线程方法的start方法:
t.start();
//播放音乐的类
class MusicRunnableImpl implements java.lang.Runnable {
    public void run() {
        for (int i = 1; i <= 50; ++i) {
            System.out.println("播放音乐" + i);
        }
    }
}

public class ImplementsRunnableDemo {
    public static void main(String[] args) {
        // 主线程:运行游戏
        for (int i = 1; i <= 50; ++i) {
            System.out.println("打游戏" + i);
            if (i == 10) {
                // 创建线程对象,启动线程
                Runnable target = new MusicRunnableImpl();
                Thread t = new Thread(target);
                t.start();
            }
        }
    }
}

还可以使用匿名内部类实现:

1.接口

public static void test1() {
    // 主线程:运行游戏
    for (int i = 1; i <= 50; ++i) {
        System.out.println("打游戏" + i);
        if (i == 10) {
            // 创建线程对象,启动线程
            new Thread(new Runnable() {
                public void run() {
                    for (int i = 1; i <= 50; ++i) {
                        System.out.println("播放电影" + i);
                    }
                }
            }).start();
        }
    }
}

2.类

public static void main(String[] args) {
    // 主线程:运行游戏
    for (int i = 1; i <= 50; ++i) {
        System.out.println("打游戏" + i);
        if (i == 10) {
            // 创建线程对象,启动线程
            new Thread() {
                public void run() {
                    for (int i = 1; i <= 50; ++i) {
                        System.out.println("播放电影" + i);
                    }
                }
            }.start();
        }
    }
}

继承方式和实现方式的区别

继承方式:
1.Java中类是单继承的,如果继承了Thread类,该类就不能有其他的直接父类;
2.从操作上分析,继承方式更简单,获取线程名字更简单(getName()),操作上更简单;
3.从多线程共享同一个资源方面来说,继承方式无法实现使用共享资源
实现方式:
1.Java中类可以实现多接口,此时该类还可以继承其他类,还可以实现其他接口(设计上更优雅);
2.从操作上分析,稍微复杂点,获取线程名字相对麻烦一点,需要使用Thread.currentThread()来获取当前线程对象的引用;
3.从多线程共享同一个资源上分析,实现方式可以做到共享资源

线程安全问题

当多线程并发访问同一个资源对象的时候可能出现线程不安全的问题

我们使用Thread.sleep()来暂停当前线程一定的时间,让其他线程运行去抢资源,用于模拟网络延迟。

结论:在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch来处理异常。
原因是:子类覆盖父类的方法的原则,子类不能抛出新的异常,在Runnable接口中的run方法没有声明抛出异常

值得注意的是,并不是使用了sleep方法之后才出现了问题,只是让问题暴露的更明显。

for (int i = 1; i <= 50; ++i) {
    if (num > 0) {
        //模拟网络延迟
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");
    }
}

应将System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");同时操作(原子操作,不能分割,必须保证同步进行)。

要解决上述读线程并发访问同一资源的安全性问题:
方式一:同步代码块
语法:

synchronized(同步锁)
{
    需要操作的代码
}

同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
也叫同步监听对象/同步锁/同步监听器/互斥锁
对象的同步锁只是一个概念,可以想象在对象上标记了一个锁。
Java程序运行使用任何对象作为同步监听对象,但是一般的,我们试验当前并发访问的共同资源作为同步监听对象。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程在外面进行等待。

public void run() {
    for (int i = 1; i <= 50; ++i) {
        synchronized (this) {//该对象表示多线程共享的资源
            if (num > 0) {
                // 模拟网络延迟
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + 
                                        "吃了编号为" + num-- + "的苹果");
            }
        }
    }
}

方式二:同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

synchronized public void run() {
    //TODO
}

同步锁是什么:
对于非static方法,同步锁就是this;
对于static方法,我们使用当前方法所在类的字节码对象(XXX.class)。

注意:不要使用synchronized修饰run方法,修饰之后某一个线程就执行完全部的功能,好比是多个线程出现串行。
解决方案:把需要同步的代码定义在新的方法中,并将该方法用synchronized修饰,再在run方法中调用该方法。

class Apple2 implements Runnable {
    private int num = 50;// 苹果总数

    public void run() {
        for (int i = 1; i <= 50; ++i) {
            eat();
        }
    }

    synchronized private void eat() {
        if (num > 0) {
            // 模拟网络延迟
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");
        }
    }
}

synchronized的优缺点:
优点:保证了多线程并发访问时的同步操作,避免了线程的安全性问题;
缺点:使用synchronized方法或代码块性能会降低。

面试题:
1.StringBuilder和StringBuffer的区别;
2.ArrayList和Vector(安全但是性能低)的区别;
3.HashMap和HashTable(安全但是性能低)的区别;
建议:尽量减小synchronized的作用域。


单例模式-懒加载模式:

public class ArrayUtil2 {
    private ArrayUtil2() {
    }

    private static ArrayUtil2 instance = null;
    public static ArrayUtil2 getInstance() {
        if (instance == null) {
            instance = new ArrayUtil2();
        }
        return instance;
    }

    public void sort(int[] arr) {

    }

但是上述代码会存在线程不安全的问题,我们使用synchronized:

public class ArrayUtil2 {
    private ArrayUtil2() {
    }

    private static ArrayUtil2 instance = null;

    // 同步方法
    synchronized public static ArrayUtil2 getInstance() {
        if (instance == null) {
            instance = new ArrayUtil2();
        }
        return instance;
    }

    public void sort(int[] arr) {

    }

但是synchronized作用域太大了,损耗性能,我们减少synchronized的作用域:
解决方案:使用双重检查加锁机制

双重检查加锁:
可以既实现线程安全,又能够使性能不受很大影响
所谓的双重检查加锁机制指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来就只需要同步一次,从而减少多次在同步情况下进行判断所浪费的时间。

“双重检查加锁”机制的实现使用volatile关键字,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能够正确处理该变量。
注意:在Java1.4以及之前版本中,很多JVM对于volatile的实现问题,会导致“双重检查加锁”的失败,因此双重检查加锁机制只能使用于Java5及以上的版本。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此建议:没有特别的需要,不要使用。

public class ArrayUtil2 {
    private ArrayUtil2() {
    }

    private static volatile ArrayUtil2 instance = null;

    // 同步方法:此时的同步监听对象是(ArrayUtil2.class)
    public static ArrayUtil2 getInstance() {
        if (instance == null) {
            synchronized (ArrayUtil2.class) {
                if (instance == null)
                    instance = new ArrayUtil2();
            }
        }
        return instance;
    }

    public void sort(int[] arr) {

    }
}

方式三:锁机制(Lock)
同步锁(Lock):
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作。Lock具有上述所有功能,而且更强大,更体现面向对象。

class Apple3 implements Runnable {
    private int num = 50;// 苹果总数

    private final Lock lock = new ReentrantLock();// 创建锁对象

    public void run() {
        for (int i = 1; i <= 50; ++i) {
            eat();
        }
    }

    private void eat() {
        // 同步代码块
        // 进入方法立刻加锁
        lock.lock();//获取锁
        try {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() 
                                    + "吃了编号为" + num + "的苹果");
                Thread.sleep(20); // 模拟网络延迟
                num--;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_42650988/article/details/81708834