Java基础——线程相关基础知识

一.线程的相关概念:

1. 并行和并发

并行:
同一时刻,可以同时处理事情的能力。

并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。

并发:
与单位时间相关,在单位时间内可以处理事情的能力。

并发是指一个处理器同时处理多个任务。

例如:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头

2. 进程和线程

进程:

     程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源

线程:

     CPU调度的最小单位,必须依赖进程而存在。

 进程和线程的区别

  • 从属关系:进程是正在运行程序的实例,进程包含了线程。
  • 描述侧重点:进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位。
  • 共享资源:多个进程不能共享资源,每个进程有自己的堆、栈、虚拟空间、文件描述等,而线程可以共享堆、方法。
  • 上下文切换速度:进程比较快(从一个线程切换到另一个线程),线程会比较慢。
  • 操作者:一般来说进程由系统控制,线程由程序员控制。
3.线程的优先级

取值为1~10,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段

4. 守护线程 & 用户线程
  • User Thread(用户线程)

普通线程,例如普通当new Thread

  • Daemon Thread(守护线程)

thread1.setDaemon(true); 也即设置了 setDaemon的线程

 当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出, 大白话就是当主线程结束时,此时普通当用户线程会继续执行,

当所有当用户线程执行完后,即结束所有线程,此时如果还有守护线程在执行,也会立即结束。

5.并发出现问题的根源: 并发三要素

可见性是由于CPU缓存引起。

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。如:

//线程1执行的代码

int i = 0;

i = 10;

//线程2执行的代码

j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

原子性问题是分时复用引起。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

如:

int i = 1;

// 线程1执行

  i += 1;

// 线程2执行

  i += 1;

1. 将变量 i 从内存读取到 CPU寄存器;

2. 在CPU寄存器中执行 i + 1 操作;

3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

有序性问题是重排序引起。

int i = 0;

boolean flag = false;

i = 1; //语句1

flag = true; //语句2

二.线程的生命周期


线程的初始化到终止的整个过程,称为线程的生命周期

新生状态

当线程对象被实例化后,就进入了新生状态。new Thread()

就绪状态

当某个线程对象调用了start()方法后,就进入了就绪状态。

在该状态下,线程对象不会做任何事情,只是在等待CPU调用。

运行状态

当某个线程对象得到运行的机会后,则进入运行状态,开始执行run()方法。

不会等待run()方法执行完毕,只要run()方法调用后,该线程就会再进入就绪状态。这时其他线程就有可能穿插其中。

阻塞状态

如果某个线程遇到了sleep()方法或wait()等方法时,就会进入阻塞状态。

sleep()方法会在一段时间后自动让线程重新就绪。

wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能进入就绪状态。

终止状态

当某个线程的run()方法中所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。

三.线程的常用方法:

四.线程同步 

为何要使用同步?

  1. 可重入锁:在执行对象中所有同步方法不用再次获得锁
  2. 可中断锁:在等待获取锁过程中可中断
  3. 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
  4. 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

 常用同步方法比较:

ReentrantLock ReentrantLock是Lock接口的实现

ReentrantLock是JDK方法,需要手动声明上锁和释放锁,因此语法相对复杂些;如果忘记释放锁容易导致死锁
锁的获取: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
锁的状态:可以判断
锁的类型:可重入 可中断 可公平(两者皆可)
性能 Lock: 大量同步 Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
6. ReentrantLock具有更好的细粒度,可以在ReentrantLock里面设置内部Condititon类,可以实现分组唤醒需要唤醒的线程

Synchronized

1.Synchoronized是JVM方法,由编辑器保证枷锁和释放,在发生异常时候会自动释放占有的锁,因此不会出现死锁
2.锁的获取:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
3.锁的状态:无法判断
4.锁的类型: 可重入 不可中断 非公平

一、两种锁的底层实现方式:
synchronized:底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。

Look底层主要靠volatile和CAS操作实现的,底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作

二、在jdk1.6~jdk1.7的时候,synchronized优化:
1、线程自旋和适应性自旋
我们知道,java’线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
2、锁消除:把不必要的同步在编译阶段进行移除。
3、锁粗化
4、轻量级锁
5、偏向锁

1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码: 
public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

 

2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

代码: 
synchronized(object){ 
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 

代码实例:

package com.xhj.thread;
    /**
     * 线程同步的运用
     * 
     * @author XIEHEJUN
     * 
     */
    public class SynchronizedThread {

        class Bank {
            private int account = 100;
            public int getAccount() {
                return account;
            }
            /**
             * 用同步方法实现
             * 
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }

            /**
             * 用同步代码块实现
             * 
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }

        class NewThread implements Runnable {
            private Bank bank;

            public NewThread(Bank bank) {
                this.bank = bank;
            }

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "账户余额为:" + bank.getAccount());
                }
            }

        }

        /**
         * 建立线程,调用内部类
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("线程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("线程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }

        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }

    }
3.使用特殊域变量(volatile)实现线程同步

代码示例:

       //只给出要修改的代码,其余代码与上同
        class Bank {
            //需要同步的变量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 
用final域,有锁保护的域和volatile域可以避免非同步的问题。 
4.使用重入锁实现线程同步

例如: 在上面例子的基础上,改写后的代码为:

代码实例: 

 

注:关于Lock对象和synchronized关键字的选择: 
    a.最好两个都不用,使用一种java.util.concurrent包提供的机制, 
        能够帮助用户处理所有与锁相关的代码。 
    b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 
    c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 
5.使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

 

例如: 在上面例子基础上,修改后的代码为:

 

6.使用阻塞队列实现线程同步

 

LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞

7.使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。

在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。

其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值

多线程相关面试题

  • 实现多线程的方式?
    • 继承Thread类

    • 实现Runnable接口后,包装为Thread对象

    • 使用匿名内部类

  • 为什么说StringBuilder是非线程

  • 什么叫死锁?怎么产生?如何解决?

    如有两个人吃西餐,必须要有刀和叉才能吃饭,只有一副刀叉。

    如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己的,

    这时就会造成死锁的局面,既不结束,也不继续。

 模拟死锁出现的原因:

定义两个线程类,线程A先获取资源A后,再获取资源B;线程B先获取资源B后,再获取资源A。

如果对A和B对使用了synchronized进行同步,就会在线程A获取资源A时候,线程B无法获取资源A,

相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。

 

 

 死锁的解决方式一:

让线程A和线程B获取资源A和资源B的顺序保持一致。

如让两个线程都先获取fork,再获取knife

死锁的解决方式二:

让线程A和线程B获取资源A和资源B之前,再获取第三个资源,并对其使用synchronized进行同步,

这样线程A在获取第三个资源后,将所有事情执行完后,线程B才能继续。

如在获取fork和knife之前,先获取paper对象

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值