一、进程、线程的区别
1、进程
进程是运行中的程序,是系统进行资源分配和调度的基本单位。
它有以下几个特征:
<1>独立性:具有私有的地址空间
<2>动态性:活动的指令集合,而程序是静态的
<3>并发性:多个进程跑在单个处理器上
2、线程
一个进程中包含了多个顺序执行流,每个顺序执行流都是一个线程。
多个线程共享同一个进程的虚拟空间,共享的环境包括:进程代码段、进程的公有数据
二、创建进程的三种方式
1、继承Thread
public class FirstThread extends Thread{
private int i=0;
@Override
public void run() {
// TODO 自动生成的方法存根
for(;i<100;i++)
System.out.println(getName()+" "+i);//可以直接调用getName()获取线程名
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
new FirstThread().start();//默认名为Thread-0
new FirstThread().start();//默认名为Thread-1
}
}
}
}
2、实现Runnable接口
public class SecondThread implements Runnable{
private int i=0;
@Override
public void run() {
// TODO 自动生成的方法存根
for(;i<100;i++)
System.out.println(Thread.currentThread().getName()+" "+i);//只能通过这种方式获取当前线程
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
SecondThread sT=new SecondThread();
new Thread(sT,"线程1").start();
new Thread(sT,"线程2").start();
}
}
}
}
3、实现Callable接口,重写call()方法
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,运行callable()可以拿到一个Future对象,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer>
{
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
创建线程的三种方式的对比:
采用实现Runnable、Callable接口的方式创见多线程时,优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
三、线程的生命周期
四、控制线程
1、join线程
当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程完成为止。
join()方法可用于将大问题划分为小问题,每个小问题分配一个线程,当所有的小问题得到处理后,再调用主线程来进一步操作。
public class JoinThread extends Thread{
//提供一个有参数的构造器用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
//重写run方法,定义线程执行体
public void run()
{
for(int i=0;i<100;i++)
System.out.println(getName()+" "+i);
}
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
//启动子线程
new JoinThread("新线程").start();
for(int i=0;i<100;i++)
{
if(i==20)
{
JoinThread jt=new JoinThread("被join的线程");
jt.start();
// main线程调用了jt线程的join方法,main线程必须等待jt执行完之后才向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
2、后台线程
后台线程的任务是为其他线程提供服务,JVM的垃圾回收线程就是典型的后台线程
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡
public class DaemonThread extends Thread{
public void run()
{
for(int i=0;i<1000;i++)
System.out.println(getName()+" "+i);
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
DaemonThread dThread=new DaemonThread();
//将此线程设置为后台进程
dThread.setDaemon(true);
//启动后台进程
dThread.start();
for(int i=0;i<10;i++)
System.out.println(Thread.currentThread().getName()+" "+i);
//---------程序执行到此处,前台进程(main进程)结束---------
//后台进程也随之结束
}
}
执行之后发现后台进程无法执行到999,因为主线程跑10次之后会结束,所以后台线程也就被结束了。
3、线程睡眠sleep
使用Thread提供的sleep()静态方法可以让当前正在执行的线程暂停一段时间进入阻塞状态
import java.util.Date;
public class TestSleep {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
for(int i=0;i<10;i++)
{
System.out.println("当前的时间是: "+new Date());
Thread.sleep(1000);
}
}
}
4、线程让步yield
yield只是让当前线程暂停一下,将该线程转入就绪状态,让系统的线程调度器重新调度一次
public class TestYield extends Thread{
public TestYield()
{}
public TestYield(String name)
{
super(name);
}
public void run()
{
for(int i=0;i<50;i++)
{
System.out.println(getName()+" "+i);
//当i=20时,使用yield方法让当前线程让步
if(i==20)
{
Thread.yield();
}
}
}
public static void main(String[] args) {
//启动两条并发进程
TestYield ty1=new TestYield("高级");
//将ty1设置为最高优先级
ty1.setPriority(Thread.MAX_PRIORITY);
ty1.start();
TestYield ty2=new TestYield("低级");
//将ty1设置为最低优先级
ty2.setPriority(Thread.MIN_PRIORITY);
ty2.start();
}
}
sleep()和wait()的区别:
1. 这两个方法来自不同的类,sleep方法属于Thread,wait方法属于Object。
2. 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。3. wait, notify和notifyAll只能在同步控制方法(synchronized)或者同步控制块里面使用,而sleep可以在任何地方使用。
4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
sleep()和yield()的区别:
1、sleep()给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会,而yield()只会给相同优先级或更高优先级的线程运行的机会。
2、线程执行sleep()后转入阻塞状态,而yield()使当前线程回到就绪状态,让OS重新执行线程调度。
五、线程同步
看一个银行取款的例子:
<1>用户输入账号、密码,系统判断是否匹配
<2>用户输入取款金额
<3>系统判断账户余额是否大于取款金
<4>如果余额大于取款金额,取款成功;如果余额小于取款金额,取款失败。
Account.class
public class Account {
//封装账户编号、账户余额
private String accountNo;
private double balance;
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;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//下面两个方法根据accoutNo来计算Account的hashCode和判断equals
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {//重写Object的equals方法,让其通过内容来判断是否相等
if(obj!=null&&obj.getClass()==Account.class)
{
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
DrawAmount.class
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;
}
//当多条线程修改同一共享数据时,将涉及数据安全问题
public void run()
{
if(account.getBalance()>=drawAmount)
{
//吐出钞票
System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);
//人为强制进行线程切换
try{
Thread.sleep(1);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}
else{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
TestDraw.java
public class TestDraw {
public static void main(String[] args) {
//创建账户
Account account=new Account("1234567",1000);
//模拟两个线程对同一账户取钱
new DrawThread("甲", account, 800).start();
new DrawThread("乙", account, 800).start();
}
}
1、使用同步代码块解决
synchronized(obj)
{
......
//此处的代码就是同步代码块
}
obj是同步监视器,即线程开始执行之前,必须获得对同步监视器的锁定;当线程执行完之后,会自然释放对该同步监视器的锁定
修改上述代码如下:
synchronized (account) {//加锁
if(account.getBalance()>=drawAmount)
{
//吐出钞票
System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);
//人为强制进行线程切换
try{
Thread.sleep(1);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}
else{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
2、同步方法
同步方法就是使用synchronized关键字来修饰某个方法,无需指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。
通过使用同步方法可以非常方便地将某类变成线程安全的类。
改写上面的例子如下:
Account.class
public class Account {
//封装账户编号、账户余额
private String accountNo;
private double balance;
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;
}
public double getBalance() {
return balance;
}
//提供一个线程安全方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
if(balance>=drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
//人为强制进行线程切换
try{
Thread.sleep(1);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
//修改余额
balance-=drawAmount;
System.out.println("\t余额为:"+balance);
}
else{
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
//下面两个方法根据accoutNo来计算Account的hashCode和判断equals
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {//重写Object的equals方法,让其通过内容来判断是否相等
if(obj!=null&&obj.getClass()==Account.class)
{
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
DrawThread.class
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;
}
public void run()
{
account.draw(drawAmount);
}
}
同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一条线程获得对Account对象的锁定,然后进入draw方法进行取钱操作
注意:
线程安全的类是以降低程序运行的效率作为代价的,不要对线程安全的类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。
3、同步锁(lock)
使用Lock时显示使用Lock对象作为同步锁,需要显式释放;而使用同步方式时系统隐式使用当前对象作为作为同步监视器
ReentrantLock锁具有重入性,即线程可以对它已经加锁的ReentrantLock锁再次加锁
private final ReentrantLock lock=new ReentrantLock();
public void draw(double drawAmount)
{
//对同步锁进行加锁
lock.lock();
try{
if(balance>=drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
//人为强制进行线程切换
try{
Thread.sleep(1);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
//修改余额
balance-=drawAmount;
System.out.println("\t余额为:"+balance);
}
else{
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
finally{
lock.unlock();
}
}
synchronized和Lock的区别
1.synchronized更容易造成死锁
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断,容易造成死锁;如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
2.ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。
3.在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态
4、lock能完成synchronized的所有功能
扩展:
1.synchronized与static synchronized 的区别
synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。那么static synchronized恰好就是要控制类的所有实例的访问了,static synchronized是限制线程同时访问jvm中该类的所有实例同时访问对应的代码快。实际上,在类中某方法或某代码块中有 synchronized,那么在生成一个该类实例后,改类也就有一个监视快,放置线程并发访问改实例synchronized保护快,而static synchronized则是所有该类的实例公用一个监视快了,也也就是两个的区别了,也就是synchronized相当于 this.synchronized,而
static synchronized相当于Something.synchronized.
一个日本作者-结成浩的《java多线程设计模式》有这样的一个列子:
pulbic class Something(){
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
那么,加入有Something类的两个实例a与b,那么下列组方法何以被1个以上线程同时访问呢
a. x.isSyncA()与x.isSyncB()
b. x.isSyncA()与y.isSyncA()
c. x.cSyncA()与y.cSyncB()
d. x.isSyncA()与Something.cSyncA()
这里,很清楚的可以判断:
a,都是对同一个实例的synchronized域访问,因此不能被同时访问
b,是针对不同实例的,因此可以同时被访问
c,因为是static synchronized,所以不同实例之间仍然会被限制,相当于Something.isSyncA()与 Something.isSyncB()了,因此不能被同时访问。
那么,第d呢?,书上的 答案是可以被同时访问的,答案理由是synchronzied的是实例方法与synchronzied的类方法由于锁定(lock)不同的原因。
个人分析也就是synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问。目前还不是分清楚java内部设计synchronzied是怎么样实现的。
结论:A: synchronized static是某个类的范围,synchronized static cSync{}防止多个线程同时访问这个 类中的synchronized static 方法。它可以对类的所有对象实例起作用。
B: synchronized 是某实例的范围,synchronized isSync(){}防止多个线程同时访问这个实例中的synchronized 方法。
2.synchronized方法与synchronized代码快的区别
synchronized methods(){} 与synchronized(this){}之间没有什么区别,只是 synchronized methods(){} 便于阅读理解,而synchronized(this){}可以更精确的控制冲突限制访问区域,有时候表现更高效率。
六、线程通信
程序通常无法准确控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。
举例说明:
假设有两条线程,分别代表存款者和取款者,系统要求存款者和取款者不断重复存款取款操作,且每当存款者将钱存入后取款者立即取出。不允许存款者连续两次存钱,也不允许取款者连续两次取钱
1、使用synchronized+Object类提供的wait()、notify()和notifyAll()三个方法
synchronized控制同步,三个方法控制协调
这三个方法必须由同步监视器对象来调用
<1>对于使用synchronized修饰的同步方法,因为默认的实例this就是同步监视器,所以可以在同步方法中直接调用
<2>对于使用synchronized修饰的同步代码块,同步监视器是synchronized括号里的对象,所以必须使用该对象来调用这三个方法
Account.class
public class Account {
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 double getBalance()
{
return this.balance;
}
public synchronized void draw(double drawAmount)//取款
{
try{
if(!flag)
{
wait();
}
else {
//取钱
System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
balance-=drawAmount;
System.out.println("账户余额为:"+balance);
//将标识设置为false
flag=false;
//唤醒其他线程
notifyAll();
}
}
catch(InterruptedException ex)
{
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount)//存款
{
try{
if(flag)
{
wait();
}
else {
//存款
System.out.println(Thread.currentThread().getName()+"存款:"+depositAmount);
balance+=depositAmount;
System.out.println("账户余额为:"+balance);
//设置flag
flag=true;
//唤醒其他线程
notifyAll();
}
}
catch(InterruptedException ex)
{
ex.printStackTrace();
}
}
}
DepositThread.class
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);
}
}
}
DrawThread.class
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);
}
}
}
TestDraw.class
public class TestDraw {
public static void main(String[] args) {
// 创建一个用户
Account account=new Account("1234567",0);
new DrawThread("取钱者", account, 800).start();
new DepositThread("存款者", account, 800).start();
}
}
运行结果:
存取款者会交替运行
2、使用Lock+Condition(条件变量)提供的await()、signal()和signalAll()三个方法
public class Account {
//显示定义Lock对象
private final Lock lock=new ReentrantLock();
//获得指定Lock对象对应的条件变量
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 double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)//取款
{
lock.lock();
try{
if(!flag)
{
wait();
}
else {
//取钱
System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
balance-=drawAmount;
System.out.println("账户余额为:"+balance);
//将标识设置为false
flag=false;
//唤醒其他线程
cond.signalAll();
}
}
catch(InterruptedException ex)
{
ex.printStackTrace();
}
finally{
lock.unlock();
}
}
public void deposit(double depositAmount)//存款
{
lock.lock();
try{
if(flag)
{
cond.await();
}
else {
//存款
System.out.println(Thread.currentThread().getName()+"存款:"+depositAmount);
balance+=depositAmount;
System.out.println("账户余额为:"+balance);
//设置flag
flag=true;
//唤醒其他线程
cond.signal();
}
}
catch(InterruptedException ex)
{
ex.printStackTrace();
}
finally{
lock.unlock();
}
}
}
3、使用管道流
前面的两种方式与其称为线程之间的通信,还不如称为线程之间协调运行的控制策略。如果需要在两条线程之间进行更多的交互,可以考虑使用管道流进行通信
public class WriterThread extends Thread {
String[] books=new String[]{
"AAAAAAAAAAAA",
"BBBBBBBBBBBB",
"CCCCCCCCCCCC",
"DDDDDDDDDDDD"
};
private PipedWriter pw;
public WriterThread(){}
public WriterThread(PipedWriter pw)
{
this.pw=pw;
}
public void run()
{
try{
//循环一百次,向管道输出流写入100个字符串
for(int i=0;i<100;i++)
{
pw.write(books[i%4]+"\n");
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
//使用finally关闭管道流
finally{
try{
if(pw!=null)
{
pw.close();
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
}
}
public class ReaderThread extends Thread{
private PipedReader pr;
//用于包装管道流的BufferReader对象
private BufferedReader br;
public ReaderThread(){}
public ReaderThread(PipedReader pr)
{
this.pr=pr;
this.br=new BufferedReader(pr);
}
public void run()
{
String buf=null;
try{
//逐行读取管道输入流的内容
while((buf=br.readLine())!=null)
{
System.out.println(buf);
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
//使用finally关闭输入流
finally {
try{
if(br!=null)
{
br.close();
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
}
}
public class PipedCommunicationTest {
public static void main(String[] args) {
PipedWriter pw=null;
PipedReader pr=null;
try{
//分别创建两个独立的管道输出流、输入流
pw=new PipedWriter();
pr=new PipedReader();
//连接管道输入流、输出流
pw.connect(pr);
//将连接好的管道流分别传入两个线程,就可以让两个线程通过管道流通信
new WriterThread(pw).start();
new ReaderThread(pr).start();
}
catch(IOException ex)
{
ex.printStackTrace();
}
}