Java学习笔记4

以下内容是从任小龙讲师课堂笔记中整理。

线程和进程

进程是指一个内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个应用程序可以同时启动多个进程。比如在windows系统中,一个运行的abc.exe就是一个进程。

线程是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程。如:多线程下载软件。
一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个执行任务,即多线程。

多进程:操作系统中同时运行多个程序
多线程:在同一个进程中同时运行多个任务。

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

因为一个进程中的多线程是并发运行的,从微观角度上考虑有先后顺序,哪个线程执行完全取决于CPU的调度,程序员控制不了。

我们可以把多线程并发看做是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,者=这也早就了多线程的随机性。

Java程序的进程至少包含主线程和垃圾回收线程。

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


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


多线程是为了同步完成多项任务,不是为了提供程序运行效率,而是通过提高资源使用效率来提高系统的效率。


Java操作进程:
在java中如何去运行一个进程
方法一:使用Runtime类的exec方法
方法二:ProcessBuilder的start方法

 

示例:创建进程

复制代码
public class test{
    public static void main(String[] args) throws Exception{
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("notepad");

        ProcessBuilder pb = new ProcessBuilder("notepad");
        pb.start();
    }
}
复制代码

 

创建和启动线程,传统有两种方式:
方式1:继承Thread类
方式2:实现Runnable接口


线程类(java.lang.Thread):Thread类和Thread子类才能称之为线程类,阅读API。
主线程(main方法运行,表示主线程)

方式1.继承Thread类
步骤:
1.定义一个类A继承于java.lang.Thread类
2.在A类中覆盖Thread类中的run方法
3.我们在run方法中编写需要执行的操作----run方法里的,线程执行体
4.在main方法(线程)中,创建线程对象,并启动线程。
创建线程类: A类 a = new A类();
调用线程对象的start方法:a.start();//启动一个线程

注意:千万不要调用run方法,如果调用run方法好比是对象调用方法,依然还是只有一个线程,并没有开启新的线程。

示例:

复制代码
class MusicThread extends Thread{
    @Override
    public void run() {
        for(int i = 1; i < 50; i++){
            System.out.println("Music . . .");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class GameThread extends Thread{
    @Override
    public void run() {
        for(int i = 1; i < 50; i++){
            System.out.println("Game . . .");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class test{
    public static void main(String[] args){
     new MusicThread().start();
     new GameThread().start();
    }
}
复制代码

 

方式2:实现Runnable接口
1.定义一个类A实现于java.lang.Runnable接口,A类不是线程类
2.在A类型中覆盖Runnable接口中的方法。
3.在run方法中编写要执行的操作:---run方法里的线程执行体
4.在main方法(线程)中,创建线程对象,并启动线程
创建线程类对象: Thread t = new Thread(new A());
调用线程对象的start()方法 t.start();

 

示例

复制代码
class MusicThread implements Runnable{
    @Override
    public void run() {
        for(int i = 1; i < 50; i++){
            System.out.println("Music . . .");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class GameThread implements Runnable{
    @Override
    public void run() {
        for(int i = 1; i < 50; i++){
            System.out.println("Game . . .");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class test{
    public static void main(String[] args){
     new Thread(new MusicThread()).start();
     new Thread(new GameThread()).start();
    }
}
复制代码

 

案例:吃苹果

存在50个苹果,并编号1-50,现在请3个同学(小A,小B,小C)上台表演吃苹果,因为A,B,C三个人可以同时吃苹果,此时得使用多线程技术实现这个案例。
分析:可以定义三个线程对象,并启动线程
每一个同学吃苹果的时候:先展示自己拿到手上的苹果编号,再吃掉苹果(意味着苹果总数减少一个)

方式1:可以使用继承Thread方式实现

复制代码
class Student extends Thread{
    private int num = 50;
    public Student(String name){
        super(name);
    }
    @Override
    public void run() {
        while(num > 0) {
            System.out.println(super.getName() + " 吃了编号为 " + (num--) + " 的苹果");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class test{
    public static void main(String[] args){
     new Student("小A").start();
     new Student("小B").start();
     new Student("小C").start();
    }
}
复制代码

 

Q:运行结果显示,使用继承方式,小A,小B、小C全部吃了50个苹果,为什么?

 

 

方式2:可以使用实现Runnable方式实现

 

复制代码
class Apple implements Runnable{
    private int num = 50;

    @Override
    public void run() {
        while(num > 0) {
            System.out.println( Thread.currentThread().getName() + " 吃了编号为 " + (num--) + " 的苹果");
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class test{
    public static void main(String[] args){
        Apple a = new Apple();
        new Thread(a,"小A").start();
        new Thread(a,"小B").start();
        new Thread(a,"小C").start();
    }
}
复制代码

当使用实现方式的时候,A,B,C一共吃了50个苹果,因为三个线程共享同一个Apple对象,而一个Apple对象中有50个苹果。

继承方式和实现方式创建线程的区别:

继承方式:
1.Java中类是单继承的,如果继承了Thread,该类就不能再有其他直接父类了
2.从操作上分析,继承方式更简单,获取线程名字也简单
3.从多线程共享同一个资源上分析,继承方式不能做到


实现方式:
1.Java中类可以实现多接口,此时类还可以继承其他类,并且还可以实现其他接口
2.从操作上分析,实现方式稍微复杂,获取线程名字也比较复杂,得使用Thread.currentThread().getName()获取线程名字
3.从多线程共享同一个资源上分析,实现方式可以做到(是否共享同一个资源,建议使用实现方式)

在这里三个同学完成抢苹果的例子的使用实现方式才合理。

 

线程不安全的问题:
当线程并发访问同一个资源对象的时候,可能出现线程不安全的问题。
但是,我们分析打印的结果发现没有问题:
意识:看不到问题,可能是我们经验不够,问题出现的不够明显。
为了让问题更明显:
Thread.sleep(10);//当前线程睡10毫秒,当前线程休息,让其他线程抢占资源,经常用来模拟网络延迟

----------------------------
在程序中并不是使用Thread.sleep(10)之后,程序才出现问题,而是使用之后,问题更明显。

为什么23号苹果被吃了两次?

A和C线程最先拿到编号为23的苹果,C线程打印结果,并进行进行num—操作,而A线程还没来得及打印,就进入睡眠。而C线程已经做了-1操作,num已经是22,所以B线程打印22.此时A线程醒来后继续打印23.

问题的原因是: 该操作分成两步,而不是原子操作
1.打印
2.num—

要解决上述多线程并发访问一个资源的安全性问题。

解决方案:保证打印和减1操作同步完成
A线程进入操作的时候,B和C线程只能在外等着,A操作结束,A,B,C才有机会进入代码执行。
-----------------------------
方式1:同步代码块
方式2:同步方法
方式3:锁机制(Lock)

 

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

 

同步代码块:
语法:
synchronized(同步锁)
{
    //需要同步操作的代码
}

同步代码块:
语法:
synchronized method()
{
    //TODO
}

 

同步锁:

 

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

 

 

 

复制代码
class Apple implements Runnable{
    private int num = 50;

    @Override
    public void run() {
        for(int i = 0; i < 50; i++) {
            synchronized (this) {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + (num) + " 的苹果");
                    try {
                        Thread.sleep(10L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num--;
                }
            }
        }
    }
}


public class test{
    public static void main(String[] args){
        Apple a = new Apple();
        new Thread(a,"小A").start();
        new Thread(a,"小B").start();
        new Thread(a,"小C").start();
    }
}
复制代码

 

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
synchronized public void doWork(){
    //TODO
}

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


不要使用synchronized修饰run方法,否则一个线程就执行完了所有功能,达不到多线程的效果。此时仍然是多线程,但是多线程是串行的。

把需要同步操作的代码块定义在一个新的方法中,且方法使用synchronized修饰,并在run方法中调用。

 

复制代码
class Apple implements Runnable{
    private int num = 50;

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

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


public class test{
    public static void main(String[] args){
    Apple a = new Apple();
    new Thread(a,"小A").start();
    new Thread(a,"小B").start();
    new Thread(a,"小C").start();
    }
}
复制代码

synchronized的好与坏:
好处:保证多线在并发访问的同步操作,避免了线程安全性问题。
缺点:使用synchronized的方法或代码块的性能比不用要低一些。

尽量减小synchronized的作用域。

单例

饿汉模式:

复制代码
public class ArrayUtil {
    private ArrayUtil(){}
    private static ArrayUtil instance = new ArrayUtil();

    public ArrayUtil getInstance(){
        return instance;
    }
}
复制代码

 

懒汉模式:

复制代码
public class ArrayUtil {
    private ArrayUtil(){}
    private static ArrayUtil instance = null;

    public static ArrayUtil getInstance(){
        if(null == instance){
            instance = new ArrayUtil();
        }
        return instance;
    }
}
复制代码

懒汉模式存在的问题:当多线程同时访问getInstance且instance等于null时将引入线程不安全问题

改进版:

复制代码
public class ArrayUtil {
    private ArrayUtil(){}
    private static ArrayUtil instance = null;

    synchronized public static ArrayUtil getInstance(){
        if(null == instance){
            instance = new ArrayUtil();
        }
        return instance;
    }
}
复制代码

使用synchronized修饰getInstance方法可解决线程不安全问题,但是引入了性能问题。

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

双重检查加锁机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

注意:在java4及以前版本中,很多JVM对于volatile关键字的实现问题会导致双重检查加锁机制失效,因此双重检查加锁机制只能用在JVM5及以上的版本。

提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此一般建议没有特别需要,不要使用。
也就是说,虽然可以使用双重检查加锁机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

复制代码
public class ArrayUtil {
    private ArrayUtil(){}
    private static volatile ArrayUtil instance = null;

     public static ArrayUtil getInstance(){
        if(null == instance){
            synchronized(ArrayUtil.class) {
                if(null == instance) {
                    instance = new ArrayUtil();
                }
            }
        }
        return instance;
    }
}
复制代码

 

 

同步锁

Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Apple implements Runnable{
    private final Lock lock = new ReentrantLock();
    private int num = 50;

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

    private void eat(){
        lock.lock();
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + (num) + " 的苹果");
            try {
                Thread.sleep(10L);
                num--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}


public class test{
    public static void main(String[] args){
        Apple a = new Apple();
        new Thread(a,"小A").start();
        new Thread(a,"小B").start();
        new Thread(a,"小C").start();
    }
}
复制代码
abstract (关键字) 抽象 ['æbstrækt] access vt.访问,存取 ['ækses]'(n.入口,使用权) algorithm n.算法 ['ælgәriðm] Annotation [java] 代码注释 [ænәu'teiʃәn] anonymous adj.匿名的[ә'nɒnimәs]'(反义:directly adv.直接地,立即[di'rektli, dai'rektli]) apply v.应用,适用 [ә'plai] application n.应用,应用程序 [,æpli'keiʃәn]' (application crash 程序崩溃) arbitrary a.任意的 ['ɑ:bitrәri] argument n.参数;争论,论据 ['ɑ:gjumәnt]'(缩写 args) assert (关键字) 断言 [ә'sә:t] ' (java 1.4 之后成为关键字) associate n.关联(同伴,伙伴) [ә'sәuʃieit] attribute n.属性(品质,特征) [ә'tribju:t] boolean (关键字) 逻辑的, 布尔型 call n.v.调用; 呼叫; [kɒ:l] circumstance n.事件(环境,状况) ['sә:kәmstәns] crash n.崩溃,破碎 [kræʃ] cohesion 内聚,黏聚,结合 [kәu'hi:ʒәn] (a class is designed with a single, well-focoused purpose. 应该不止这点) command n. 命令,指令 [kә'mɑ:nd](指挥, 控制) (command-line 命令行) Comments [java] 文本注释 ['kɒments] compile [java] v.编译 [kәm'pail]' Compilation n.编辑[,kɒmpi'leiʃәn] const (保留字) constant n. 常量, 常数, 恒量 ['kɒnstәnt] continue (关键字) coupling 耦合,联结 ['kʌpliŋ] making sure that classes know about other classes only through their APIs. declare [java] 声明 [di'klєә] default (关键字) 默认值; 缺省值 [di'fɒ:lt] delimiter 定义符; 定界符 Encapsulation[java] 封装 (hiding implementation details) Exception [java] 例外; 异常 [ik'sepʃәn] entry n.登录项, 输入项, 条目['entri] enum (关键字) execute vt.执行 ['eksikju:t] exhibit v.显示, 陈列 [ig'zibit] exist 存在, 发生 [ig'zist] '(SQL关键字 exists) extends (关键字) 继承、扩展 [ik'stend] false (关键字) final (关键字) finally (关键字) fragments 段落; 代码块 ['frægmәnt] FrameWork [java] 结构,框架 ['freimwә:k] Generic [java] 泛型 [dʒi'nerik] goto (保留字) 跳转 heap n.堆 [hi:p] implements (关键字) 实现 ['implim
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值