多线程

一.说明类java.lang.ThreadLocal 的作用和原理。列举在哪些程序中见过ThreadLocal 的使用?

作用:
要编写一个多线程安全(Thread-safe)的程序是困难的,为了让线程共享资源, 必须小心地对共享资源进行同步,同步带来一定的效能延迟,而另一方面, 在处理同步的时候,又要注意对象的锁定与释放,避免产生死结,种种因素 都使得编写多线程程序变得困难。
尝试从另一个角度来思考多线程共享资源的问题,既然共享资源这么困难, 那么就干脆不要共享,何不为每个线程创造一个资源的复本。将每一个线程存取数据的行为加以隔离,实现的方法就是给予每个线程一个特定空间来保管该线程所独享的资源。
比如:在 Hibernate 中的 Session 就有使用。
ThreadLocal 的原理
ThreadLocal 是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在 ThreadLocal 类中有一个 Map,用于存储每一个线程的变量的副本。

二.说说乐观锁与悲观锁

答:
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时
候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition 机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行 retry, 这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

三.在Java 中怎么实现多线程?描述线程状态的变化过程。

答:当多个线程访问同一个数据时,容易出现线程安全问题,需要某种方式来确保资源在某一时刻只被一个线程使用。需要让线程同步,保证数据安全线程同步的实现方案:同步代码块和同步方法,均需要使用 synchronized 关键字
同步代码块:public void makeWithdrawal(int amt) { synchronized (acct) { }
}
同步方法:public synchronized void makeWithdrawal(int amt) { }
线程同步的好处:解决了线程安全问题
线程同步的缺点:性能下降,可能会带来死锁
Lock锁

四.在多线程编程里,wait 方法的调用方式是怎样的?

答:
wait 方法是线程通信的方法之一,必须用在 synchronized 方法或者synchronized 代码块中,否则会抛出异常,这就涉及到一个“锁”的概念, 而 wait 方法必须使用上锁的对象来调用,从而持有该对象的锁进入线程等待状态,直到使用该上锁的对象调用 notify 或者 notifyAll 方法来唤醒之前进入等待的线程,以释放持有的锁。

五.volatile 关键字是否能保证线程安全?

答:
不能。虽然 volatile 提供了同步的机制,但是只是一种弱的同步机制,如需要强线程安全,还需要使用 synchronized。
Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
一、volatile 的内存语义是:
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。二、volatile 底层的实现机制
如果把加入volatile 关键字的代码和未加入volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个 lock 前缀指令。
1 、重排序时不能把后面的指令重排序到内存屏障之前的位置
2、使得本 CPU 的 Cache 写入内存
3、写入动作也会引起别的 CPU 或者别的内核无效化其 Cache,相当于让新写入的值对别的线程可见。

六.Java 线程的几种状态

答:
线程是一个动态执行的过程,它有一个从产生到死亡的过程,共五种状态:
新建(new Thread)
当创建 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动) 例如:Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队等候得到 CPU 资源。例如:t1.start();
运行(running)
线程获得 CPU 资源正在执行任务(run()方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行 run()方法后终止
异常终止:调用 stop()方法让一个线程终止运行
堵塞(blocked)
由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。
正在睡眠:用 sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用 wait()方法。(调用 motify()方法回到就绪状态)
被另一个线程所阻塞:调用 suspend()方法。(调用 resume()方法恢复)

七.进程和线程的区别是什么?

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

进程线程
作为资源分配的单位调度和执行的单位
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。线程可以看成时轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程
切换的开销小。
在操作系统中能同时运行多个任务(程序)在同一应用程序中有多个顺序流同时执行
系统在运行的时候会为每个进程分配不同的内存区域除了 CPU 之外,不会为线程分配内存线程组只能共享资源
没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而多条线程共同完成的。线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。

八.同步和异步有何异同,在什么情况下分别使用它们?

答:
1.如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到, 或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
2.当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
3.举个例子: 打电话是同步 发消息是异步

九.Java 线程中,sleep()和wait()区别

答:
sleep 是线程类(Thread)的方法;作用是导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复;调用sleep()不会释放对象锁。
wait 是 Object 类的方法;对此对象调用 wait 方法导致本线程放弃对象锁, 进入等 待此对象的等待锁定池。只有针对此对象发出 notify 方法(或notifyAll)后本线程才进入对象锁定池,准备获得对象锁进行运行状态。

十.sleep和yield的区别

答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
④ sleep()方法比 yield()方法(跟操作系统相关)具有更好的可移植性。

十一.线程同步相关的方法

答:
1.wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
2.sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException 异常;
3.notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
4.notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争;
5.JDK 1.5 通过 Lock 接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。Lock 接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了 newCondition()方法来产生用于线程之间通信的Condition 对象;
JDK 1.5 还提供了信号量(semaphore)机制,信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用 Semaphore 对象的 acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用 Semaphore 对象的 release() 方法)

下面的例子演示了 100 个线程同时向一个银行账户中存入 1 元钱,在没有
使用同步机制和使用同步机制情况下的执行情况。

银行账户类:

package com.bjsxt;
/**
*银行账户
*
*/
public class Account {

private double balance;//账户余额

**
*存款
*@param money 存入金额
*/
public void deposit(double money) { double newBalance = balance + money; try {
Thread.sleep(10);	// 模拟此业务需要一段处理时间
}
catch(InterruptedException ex) { ex.printStackTrace();
}
balance = newBalance;
}
/**
*获得账户余额
*/
public double getBalance() {
return balance;
}
}

存钱线程类:

package com.bjsxt;
/**
*存钱线程
*
*/
public class AddMoneyThread implements Runnable {
private Account account;	// 存入账户

private double money; //存入金额

public AddMoneyThread(Account account, double money) {
this.account = account;
this.money = money;
}

@Override
public void run() { account.deposit(money);
}

测试类:

package com.bjsxt;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test01 {
public static void main(String[] args) { Account account = new Account(); ExecutorService service =
Executors.newFixedThreadPool(100);
for(int i = 1; i <= 100; i++) {
service.execute(new AddMoneyThread(account, 1));
}
service.shutdown();
while(!service.isTerminated()) {} System.out.println("账户余额: " +
account.getBalance());
}
}

在没有同步的情况下,执行结果通常是显示账户余额在 10 元以下,出现这种状况的原因是,当一个线程 A 试图存入 1 元的时候,另外一个线程 B 也能够进入存款的方法中,线程 B 读取到的账户余额仍然是线程 A 存入 1 元钱之前的账户余额,因此也是在原来的余额 0 上面做了加 1 元的操作,同理线程 C 也会做类似的事情,所以最后 100 个线程执行结束时,本来期望账户余额为 100 元,但
实际得到的通常在 10 元以下。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作, 代码有如下几种调整方案:

  1. 在银行账户的存款(deposit)方法上同步(synchronized)关键字
package com.bjsxt;
/**
*银行账户
*/
public class Account {

private double balance;  //账户余额

/**
*存款
*@param money 存入金额
*/
public synchronized void deposit(double money) {

double newBalance = balance + money;
try {
Thread.sleep(10);	// 模拟此业务需要一段处理时间
}
catch(InterruptedException ex) { ex.printStackTrace();
}
balance = newBalance;
}

/**
* 获得账户余额
*/
public double getBalance() {
return balance;
}
}
  1. 在线程调用存款方法时对银行账户进行同步
ackage com.bjsxt;
/**
*存钱线程
*
*/
public class AddMoneyThread implements Runnable {
private Account account;	// 存入账户

private double money; //存入金额

public AddMoneyThread(Account account, double money) {
this.account = account;
this.money = money;
}
@Override
public void run() {
synchronized (account) { account.deposit(money);
}
}
}
  1. 通过 JDK 1.5 显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
package com.bjsxt;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*银行账户
*
*
*/
public class Account {
private Lock accountLock = new ReentrantLock();
private double balance; // 账户余额
/**
*存款
*
*@param money  存入金额
*/
public void deposit(double money) { accountLock.lock();
try {
double newBalance = balance + money;
try {
Thread.sleep(10); // 模拟此业务需要一段处理时间

}
catch (InterruptedException ex) { ex.printStackTrace();
}
balance = newBalance;
}
finally {
accountLock.unlock();
}
}
/**
* 获得账户余额
*/
public double getBalance() {
return balance;
}
}

按照上述三种方式对代码进行修改后,重写执行测试代码 Test01,将看到最终的账户余额为 100 元。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值