一、基本的线程机制
并发编程使我们将程序划分为多个分离的,独立运行的任务。通过使用多线程机制,这些任务有独立线程驱动。
单个进程可以拥有多个并发执行的任务(即多个线程),cpu轮流为每个任务分配占用时间。
1、定义任务
任务有线程驱动,如何定义任务?
实现Runnable借口,并重写run()方法。
//一个实现Runnable的类
class MyRunnale implements Runnable
{
private static int count=1;
@Override
public void run() {
System.out.println("第"+count+++"次打印");
}
}
但是实现runnable借口之后的run方法没有特别之处,要具有线程能力,必须把他提交给一个Thread构造器。
public class Test {
public static void main(String []args)
{
for(int i=0;i<3;i++)
{
//把runnable对象提交给Thread类,来获得线程能力
Thread thread=new Thread(new MyRunnale());
thread.start();//必须调用start()方法来启动线程
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程");
}
}
注意:要启动线程必须用start()方法来显式启动。另外,main()本身也是一个主线程。
要获取线程得名字,可用Thread.currentThread.getName()方法。设置名字可用setName()方法。
3、使用Executor来管理线程对象
Executor在客户端和任务执行之间提供了一个中间层,这个中间对象负责执行任务。
使用CachedThreadPool为每个任务创建一个线程,用executorService来执行Runnable对象。
ExecutorService es= Executors.newCachedThreadPool();
es.excute(Runnable对象);
es.shutdown();
4、通过继承Thread类来实现线程能力
//通过继承Thread类获得线程能力
public class Test extends Thread{
public static void main(String []args)
{
new Test().start();
System.out.println("主线程");
}
public String toString()
{
return Thread.currentThread().getName();
}
//重写run方法
public void run()
{
System.out.println("子线程");
}
}
5、线程休眠,线程让步,线程优先级和加入线程
线程休眠Thread.sleep(time 毫秒):让任务中止执行给定的时间,sleep方法是Thread类的静态方法,可直接类名.sleep()调用。
调用sleep方法后,该线程将被阻塞,线程调度器切换到另一个线程,进而驱动另一个任务。
线程优先级(priority):通过设置线程得优先级可让优先级高的线程执行频率变高,但不能保证高优先级的线程一定优先执行,
线程的执行顺序是无法确定的。通过setpriority()和getpriority()设置和读取线程的优先级。
线程让步Thread.yield():让当前任务终止,去执行具有相同优先级的其他线程。
加入线程:在一个线程run方法内部加入join()方法,可使当前线程将被挂起,直到另一个加入的线程运行完毕后继续运行。
也可在join方法内设定一个时间参数,到一定的时间后就会返回执行原来的线程。
6、共享受限资源
共享资源竞争:java并发会引起两个或多个线程彼此相互干渉,如两个线程同时改变一个值或同时访问一个银行账户。
class MyRunnale implements Runnable {
private static int i=0;
@Override
//对于可能发生资源竞争的i没有采取同步
public void run() {
System.out.println(Thread.currentThread().getName()+" "+(i++));
}
}
//测试类
public class Test {
public static void main(String []args)
{
for (int i=0;i<50;i++)
{
new Thread(new MyRunnale()).start();
}
}
运行结果出现了i并没有加到49,而是48。说明有两个线程同时访问了i,并自增。
Thread-0 0
Thread-1 1
Thread-2 2
Thread-3 3
Thread-4 4
Thread-5 5
Thread-6 6
Thread-7 7
Thread-8 8
Thread-9 9
Thread-10 10
Thread-11 11
Thread-12 12
Thread-14 13
Thread-13 14
Thread-15 15
Thread-16 16
Thread-17 17
Thread-18 18
Thread-19 19
Thread-20 20
Thread-21 21
Thread-22 22
Thread-23 23
Thread-24 24
Thread-25 25
Thread-26 26
Thread-27 27
Thread-28 28
Thread-29 29
Thread-30 30
Thread-32 31
Thread-31 32
Thread-33 33
Thread-34 34
Thread-35 35
Thread-36 36
Thread-37 37
Thread-38 38
Thread-39 39
Thread-42 40
Thread-40 41
Thread-49 42
Thread-41 43
Thread-43 44
Thread-46 46
Thread-45 47
Thread-48 42
Thread-44 48
Thread-47 45
如何解决共享资源竞争? 在任务上面加锁,在给定时刻只允许一个任务访问共享资源。
如何同步?
1)
java通过关键字synchronized关键字加锁。当任务执行到要被synchronized关键字保护的代码片段时,会检查锁
是否可用,获取锁,执行代码,然后释放锁。
class MyRunnale implements Runnable {
private static int i=0;
@Override
public void run() {
synchronized (this)
{
System.out.println(Thread.currentThread().getName()+" "+(i++));
}
}
}
2)使用显式的lock对象
对要保护的代码片段使用lock()方法显式的加锁,且lock对象必须是static(保证每个线程使用同一把锁)的,同步代码块后必须用unlock()释放锁。
class MyRunnale implements Runnable {
private static int i=0;
private static Lock lock=new ReentrantLock();
@Override
public void run() {
//显式加锁
lock.lock();
{
System.out.println(Thread.currentThread().getName() + " " + (i++));
}
//释放锁
lock.unlock();
}
}
所有对象都自动含有单一的锁,也就是每个对象只有一个锁。如果一个对象含有多个synchronized方法,这些方法共享
一把锁。其他synchronized方法只有等前一个方法调用完毕后才能被调用。
注意:在使用并发时,将域设置为private是重要的。可以防止其他任务直接访问域,而不是通过synchronized方法访问。
一个任务可以多次获得对象的锁。当一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象上的另一个方法,
就会出现这种状况。也就是说只有首先获取了锁的任务才能继续获取多个锁。每次任务进入一个方法时,计数器会加一,离开就
减一,直到变为0后,完全释放锁。
同步规则:一个变量可能被多个线程读和写,必须使用同步。读写线程必须使用相同的锁。
7、原子性和可视性
原子性:除long和double之外的所有基本类型上的简单操作(读取和写入),都是原子性操作。
在long和double变量前加上volatile关键字可以获得原子性。
int i=0;
i++;//不是原子操作,获取了i,再把i自增,再赋值给i。中间包含了一个自增步骤,不是线程安全的
i+=2;//不是原子操作,中间包含对i加2的操作
i=2;//原子操作
可视性:一个线程对一个域进行了写操作,那么所有的读操作都会看到这个修改。使用violate关键字可以保证可视性。
violate域会立即被写入到主存中,而读取操作发生在主存中。
注意 :可视性无法保证线程安全。可视性只是保证每次读操作读取到的都是最新的值。如上面的例子,
i设置为violate无法保证线程安全。当一个线程从主存中读取了i的值,但没有进行自增操作,另一线程从主存中
也读取了i,i的值没变,那么两个线程结束后的到的i值会是一样的。
8、线程状态
1)新建状态(New):新创建了一个线程对象。
2.)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3) 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4) 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5). 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程状态图
9、线程之间的协作
定义:线程之间的协作指的是多个线程在运行时保证某些线程必须在其他线程运行之前或之后运行。
如吃饭前必须把做饭这个任务先完成。
如何实现线程之间的协作?
方法一:synchronized下使用wait()和notifyall(),必须保证每次wait()和notifyall()使用的对象锁必须是同一个,不然会无法唤醒。
class A
{
public synchronized void f1()
{
System.out.println("同步方法一");
try {
System.out.println("方法一被挂起");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("方法一被唤醒");
}
public synchronized void f2()
{
System.out.println("同步方法二");
notifyAll();
}
}
class MyRunnale implements Runnable {
A a;
public MyRunnale( A a)
{
this.a=a;
}
@Override
public void run() {
a.f1();
}
}
class MyRunnable1 implements Runnable
{
A a;
public MyRunnable1(A a)
{
this.a=a;
}
@Override
public void run() {
a.f2();
}
}
//测试类
public class Test {
public static void main(String []args)
{
//保证同步方法使用同一把对象锁a,这样能够被notifyall()唤醒
A a=new A();
new Thread(new MyRunnale(a)).start();
new Thread(new MyRunnable1(a)).start();
}
}
方法二:显式的lock下使用condition对象
class A
{
private static Lock lock=new ReentrantLock();
private static Condition condition=lock.newCondition();//condition对象
public void f1()
{
//显式lock对象
lock.lock();
System.out.println("同步方法一");
try {
System.out.println("方法一被挂起");
condition.await();//condition对象的await方法,挂起线程,释放同步锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("方法一被唤醒");
lock.unlock();
}
public void f2()
{
lock.lock();
System.out.println("同步方法二");
condition.signalAll();//唤醒等待线程
lock.unlock();
}
}
10、死锁
定义:某个任务等待另一个任务,而后者又等待别的任务,一直下去,最后一个任务又在等待第一个任务释放锁,循环等待,
没有哪一个任务能继续运行,就产生了死锁。 经典死锁问题:哲学家就餐问题。
产生死锁条件需要同时满足一下四个条件:
1)互斥条件:任务使用的资源中至少有一个是不能共享的。如一根筷子一次只能被一个哲学家使用
2)至少有一个任务它必须有一个资源且正则等待获取一个当前被别的任务持有的资源。要产生死锁,哲学家必须拿着一根
筷子,并且等待另一根筷子。
3)资源不能被抢占
4)必须有循环等待
要防止死锁,最容易的方法就是破坏第四个条件,每个哲学家不一定都先从右手边拿筷子,这样就破坏了循环等待。