java基础之 多线程

总结学习,我认为是一个非常好的学习方法。

写在前面【摘】:

作为一名Java开发人员,不管作为面试官,还是被面试的对象,甚至是两者兼有。Java线程技术的考察,势必成为整个面试过程的重点之一。分析一下原因,不难发现,实际工作当中,涉及到的Java应用几乎全是多线程,单线程Java应用微乎其微。如何管理好多线程的调度,比如线程的安全问题,是Java应用实现高效并行运行的关键点之一,也是摆在大多数Java初学者的难题。


多线程

我从以下几个方面进行知识总结:

一、概述

操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

注:现代的操作系统都支持多进程的并发,但在具体的实现细节上可能因为硬件和操作系统的不同而采用不同的策略:如共用式、抢占式等。

        一般,进程包含如下三个特征:

       (1)独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

       (2)动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。

       (3)并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。

二、线程创建和启动的几种方法

1、继承Thread 类

      new MyThread().start();

2、实现Runnable 接口

      MyRunnable mr = new MyRunnable();

      new Thread(mr , "新线程").start();

两种方法的优缺点:

a、实现Runnable接口创建多线程

优点:

(1)还可以实现其他接口,继承其他类。

(2)在这种情况下,多个线程可以共享同一个target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

缺点:

编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread() 方法。

b、采用继承Thread 类的方式创建多线程的

优点:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread() 方法,直接使用this 即可获得当前线程。

缺点:已经继承了Thread 类,就不能再继承其他父类。

综上所述:一般采用 Runnable  接口创建多线程。


三、线程的生命周期、线程的控制

三、线程同步

1、为什么要线程同步:

线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

2、实现线程同步的几种方法:

(1)同步监视器(Synchronized)

<1>从使用方式上,Synchronized包含两种用法:①Synchronized 方法和 ②Synchronized 块。

①、前者在方法声明前加synchronized关键字,如:


public synchronized void accessVal(int newVal);
用来控制对共享资源的访问。同一时间,仅允许一个线程操作该方法,其它线程排队。

②、后者在代码块前加synchronized关键字,如:

synchronized(syncObject) {
  //允许访问控制的代码
}
和前者一样,同一时间,仅允许一个线程进入该代码块。
两者的唯一区别是,Synchronized 方法对应的锁对象是this,而Synchronized 块对应的锁对象可以自由指定。

<2>从作用域的角度,Synchronized也分为两种:实例对象和类对象。synchronized aMethod(){}是一个实例方法,这就意味着,对于同一个对象,同一时间,仅有可能被一个线程调用,其它线程排队。而对于与不同的对象,其它线程仍然能访问该方法。所不同的是,对于类对象而言,情况则完全不同。synchronized static aStaicMethod(){}是一个类方法,这就意味着,同一时间,该方法只能被一个线程调用,其它的线程没有任何机会。除非当前线程执行完操作或终止,释放线程锁。

<3>Synchronized 经典实战:生产者与消费者【摘】

线程的同步最经典的案例莫过于生产者与消费者问题。我们的例子将围绕它展开。生产者与消费者指的是两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者线程,用于将“产品”放入缓冲区;另一个是消费者线程,用于从缓冲区取出“产品”。问题出现在缓冲区已满,生产者还想添加“产品”,其解决办法是让生产者此时休眠,待消费者从缓冲区取走一个或者多个“产品”再唤醒。同样地,当缓冲区已空,消费者还想取出“产品”,此时也可以让消费者休眠,待生产者放入一个或者多个数据时再唤醒它。

package thread.synchronizedTest;
/**
 * 假设缓冲区的长度为1,这里的“产品”对应一个整数。该缓冲区提供读、写操作。对应的Buffer类
 *
 */
public class Buffer {
int n;
boolean hasValue = false;


synchronized int get() {
while (!hasValue)
try {
wait();


} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println("Get: " + n);
hasValue = false;
notify();
return n;
}


synchronized void put(int n) {
while (hasValue)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.n = n;
hasValue = true;
System.out.println("Put: " + n);
notify();
}
}

package thread.synchronizedTest;
/**
 * 生产者线程
 */
class Producer implements Runnable {
Buffer q;
Producer(Buffer q) {
this.q = q;
new Thread(this, "Producer").start();
}


public void run() {
int i = 0;
while (true) {
q.put(i++);
}
}
}

package thread.synchronizedTest;
/**
 * 消费者线程
 *
 */
public class Consumer implements Runnable {
Buffer q;
Consumer(Buffer q) {
this.q = q;
new Thread(this, "Consumer").start();
}


public void run() {
while (true) {
q.get();
}
}
}

package thread.synchronizedTest;
/**
 * Main 方法
 */
public class Sample {

public static void main(String args[]) {
Buffer q = new Buffer();
new Consumer(q);
new Producer(q);
}
}

(2)同步锁(Lock)

Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的对象。

Lock是控制多个线程对共享资源进行访问的哦给你工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

class X {
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// ...
// 定义需要保证线程安全的方法
public void m() {
// 加锁
lock.lock();
try{
// 需要保证线程安全的代码
// ... method body
}
// 使用finally 块来保证释放锁
finally{
lock.unlock();
}
}
}

注:某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁), Lock、ReadWriteLock 是 java5 提供的两个接口,并为Lock 提供了 ReentrantLock (可重入锁),为ReedWriteLock 提供了ReentrantReadWriteLock 实现类。

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock 锁再次加锁,ReentrantLcok 对象会维持一个计数器来追踪Lock() 方法的嵌套调用,线程在每次调用lock() 加锁后,必须显式调用unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

(3)死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,java 虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。

实例:

package deadLock;


public class A {
public synchronized void foo(B b) {
System.out.println("当前线程名:" + Thread.currentThread().getName() + "进入了A实例的foo()方法");
try{
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:" + Thread.currentThread().getName() + "企图调用B实例的last() 方法");
b.last();
}
public synchronized void last() {
System.out.println("进入了A类last() 方法内部");
}
}

package deadLock;


public class B {
public synchronized void bar(A a) {
System.out.println("当前线程名:" + Thread.currentThread().getName() + "进入了B实例的bar() 方法");
try{
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:" + Thread.currentThread().getName() + " 企图调用A实例的last()方法");
a.last();
}
public synchronized void last() {
System.out.println("进入了B 类的last()方法内部");
}
}

package deadLock;


public class DeadLock implements Runnable {
A a = new A();
B b = new B();

public void init() {
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入到了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar() 方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
// 以d1 为 target 启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

注意:由于Thread 类的suspend() 方法也很容易导致死锁,所以java 不再推荐使用该方法来暂停线程的执行。

注:synchronized 与 Lock的区别:

1.synchronized安全,Lock不安全安全与不安全是指是否会引起死锁。
synchronized关键字是安全的,因为它在临界区开始处加锁,临界区结束处解锁,这是由虚拟机控制的,不会有错。但是使用Lock时要注意,不能忘记解锁,否者会死锁。
2.Lock要比synchronized效率高
3.Lock最大优势在于它分为读锁和写锁,而synchronized没有
Lock的读锁允许多个线程一起读,当然,不允许一边读一边写,也允许多个线程同时写;而synchronized只能让一个线程操作,无论读写。


四、线程通讯【摘】

(1)线程的协调运行
场景:用2个线程,这2个线程分别代表存款和取款。——现在系统要求存款者和取款者不断重复的存款和取款的动作,而且每当存款者将钱存入账户后,取款者立即取出这笔钱。不允许2次连续存款、2次连续取款。实现上述场景需要用到Object类,提供的wait、notify和notifyAll三个方法,这3个方法并不属于Thread类。但这3个方法必须由同步监视器调用,可分为2种情况:
A、对于使用synchronized修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步中直接调用这3个方法。
B、对于使用synchronized修改的同步代码块,同步监视器是synchronized后可括号中的对象,所以必须使用括号中的对象调用这3个方法
方法概述:
一、wait方法:导致当前线程进入等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。wait方法有3中形式:无参数的wait方法,会一直等待,直到其他线程通知;带毫秒参数的wait和微妙参数的wait,这2种形式都是等待时间到达后苏醒。调用wait方法的当前线程会释放对该对象同步监视器的锁定。
二、notify:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(用wait方法),才可以执行被唤醒的线程。
三、notifyAll:唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才能执行唤醒的线程。
(2)、条件变量控制协调
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也不能使用wait、notify、notifyAll方法来协调进程的运行。
当使用Lock对象同步,Java提供一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法组合使用,为每个对象提供了多个等待集(wait-set),这种情况下,Lock替代了同步方法和同步代码块,Condition替代同步监视器的功能。Condition实例实质上被绑定在一个Lock对象上,要获得特定的Lock实例的Condition实例,调用Lock对象的newCondition即可。
Condition类方法介绍:
一、await:类似于隐式同步监视器上的wait方法,导致当前程序等待,直到其他线程调用Condition的signal方法和signalAll方法来唤醒该线程。 该await方法有跟多获取变体:long awaitNanos(long nanosTimeout),void awaitUninterruptibly()、awaitUntil(Date daadline)
二、signal:唤醒在此Lock对象上等待的单个线程,如果所有的线程都在该Lock对象上等待,则会选择随机唤醒其中一个线程。只有当前线程放弃对该Lock对象的锁定后,使用await方法,才可以唤醒在执行的线程。
三、signalAll:唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

实例:

package thread.Condition;


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
// 显示定义  Lock 对象
private final Lock lock = new ReentrantLock();
// 获得指定Lock 对象对应的 Condition
private final Condition cond = lock.newCondition();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
// 标志账户中是否已有存款的旗帜
private boolean flag = false;
public Account() {}
// 构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
// 账户余额不允许修改,所以,只提供getter方法
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
// 加锁
lock.lock();
try{
// 如果flag 为假,表名账户中还没有人存钱进去,取钱方法阻塞
if(!flag) {
cond.await();
}else{
// 执行取钱操作
System.out.println(Thread.currentThread().getName() + " 取钱: " + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为 false
flag = false;
// 唤醒其他线程
cond.signalAll();
}
} catch(InterruptedException e) {
e.printStackTrace();
}
// 使用finally 块儿来释放锁
finally{
lock.unlock();
}
}
public void deposit(double depositAmount) {
lock.lock();
try{
// 如果flag 为真,表明账户中已有人存钱进去,存钱方法阻塞
if(flag) {
cond.await();
}else{
// 执行存款操作
System.out.println(Thread.currentThread().getName() + "存款: " + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的标志设为true
flag = true;
// 唤醒其他线程
cond.signalAll();
}
} catch(InterruptedException e){
e.printStackTrace();
}
// 使用finally 块来释放锁
finally{
lock.unlock();
}
}
// 下面两个方法根据accountNo 来重写hashCode() 和 equals() 方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

package thread.Condition;
/**
 * 存钱
 *
 */
public class DepositThread extends Thread{
// 模拟用户帐户
private Account account;
// 当前存款线程所希望存的钱数
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
// 重复100 次执行存款操作
public void run() {
for(int i = 0; i < 100; i++) {
account.deposit(depositAmount);
}
}
}

package thread.Condition;
/**
 * 取钱的线程类 
 */
public class DrawThread extends Thread{
// 模拟用户帐户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 重复100 次执行取钱操作
public void run() {
for(int i = 0; i < 100; i++) {
account.draw(drawAmount);
}
}
}

package thread.Condition;


public class DrawTest {
public static void main(String[] args) {
// 创建一个账户
Account acct = new Account("1234567", 1000);
new DrawThread("取钱者", acct, 800).start();
new DepositThread("存钱人", acct, 800).start();
}
}
(3)、使用管道流
线程通信使用管道流,管道流有3种形式:
PipedInputStream、PipedOutputStream、PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,它们分别是管道流的字节流、管道字符流和新IO的管道Channel。
管道流通信基本步骤:
A、使用new操作法来创建管道输入、输出流
B、使用管道输入流、输出流的connect方法把2个输入、输出流连接起来
C、将管道输入、输出流分别传入2个线程
D、2个线程可以分别依赖各自的管道输入流、管道输出流进行通信


注:文章部分内容参考:http://tech.it168.com/a2012/0131/1305/000001305256.shtml


发布了28 篇原创文章 · 获赞 21 · 访问量 17万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章