java多线程
多线程的优点:
1)资源利用率更好
2)程序设计在某些情况下更简单
3)程序响应更快
多线程的代价:
1)设计更复杂
虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。
2)上下文切换的开销
当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。
进程就是应用程序在内存中分配空间。(正在硬运行的程序)内存:是暂时存储;硬盘:是持久化存储
线程:是进程中负责程序执行的执行单元。也称为执行路径
一个进程中至少有一个线程在负责该进程的运行。
如果一个进程中出现了多个线程,就称为该程序为多线程程序。
多线程(例如:一个人(CPU)给多个孩子(线程)喂饭)很生动形象
创建多线程:有两种形式(1)继承Thread类;(2)实现Runnable接口
(1)继承Thread类
线程的任务都封装在特定的区域中比如:主线程运行的任务都定义在main方法中。垃圾回收线程在收垃圾都会运行finalize方法。
start调用线程 start主要是做了两件是:1、开启线程2、调用run方法
Thread.currentThread().getName()
调用run不会开启线程,只有主线程在执行
import java.net.Socket;
public class Thread1 extends Thread {
private static int socket = 100;
public void run()
{
while(true){
if(socket>0){
System.out.println(Thread.currentThread().getName()+socket--);
}
}
}
}public class Demo1 {
public static void main(String[] args) {
Thread1 thread1=new Thread1();
Thread1 thread2=new Thread1();
Thread1 thread3=new Thread1();
Thread1 thread4=new Thread1();
thread1.start();
thread2.start();
thread3.start();
thread4.start();}
start开启线程,让开启的线程去执行run方法中的线程任务(run里面运行的是你自定义的线程对象)
run调用方法(线程并未开启,去执行run方法的是主线程)
线程什么时候结束:当线程中没有了方法后就回收该线程
(2)实现Runnable接口
创建线程的第二种方式,实现Runnable接口。
1、定义一个类实现Runnable。
2、覆盖Runnable接口中的run方法,将线程要运行的任务代码存储到该方法中。
3、通过Thread类创建线程对象,并将实现了Runnable接口的对象作为Thread类的构造函数的参数进行传递。
4、调用Thread类的start方法,开启线程。
实现Runnable接口的好处
1、避免了继承Thread类的单继承的局限性
2、Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
3、Runnable接口出现,降低了线程对象和线程任务的耦合性。
所以,以后创建线程都是用第二种方式。
public class ThreadPractice implements Runnable {
private static int socket = 100;
public void run() {
while (true) {
if (socket > 0) {
System.out.println(Thread.currentThread().getName() + "..." + "socket" + "..." + socket--);
}
}}}public class Demo1 {
public static void main(String[] args) {
ThreadPractice threadPractice=new ThreadPractice();
Thread t1=new Thread(threadPractice);
Thread t2=new Thread(threadPractice);
Thread t3=new Thread(threadPractice);
Thread t4=new Thread(threadPractice);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
多线程的安全问题
产生的原因:
1、线程任务中有处理到共享的资源
2、线程任务中有多条对共享数据的操作。
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
一个线程在操作共享数据的过程中,其他线程参与了运算,造成了数据的错误。
解决的思想:
只要保证多条操作共享数据的代码在某一时间段,被一条线程所执行,在执行期间不允许其他线程参与运算。
怎么保证呢?
用到同步代码块。
Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
有四种不同的同步块:
- 实例方法
- 静态方法
- 实例方法中的同步块
- 静态方法中的同步块
synchronized(对象)
{
需要被同步的代码。
}
代码示例:
synchronized (object) {
while (true) {
if (socket > 0) {
System.out.println(Thread.currentThread().getName() + "..." + "socket" + "..." + socket--);
}
}
}
同步在目前情况下保证了一次只能有一个线程在执行,其他线程进不来
这就是同步的锁机制。
好处:解决了多线程的安全问题。
弊端:减低效率。
有可能出现这样一种情况:
多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧?
这时肯定是同步出了问题。
只要遵守了同步的前提,就可以解决。
同步的前提:
多个线程在同步中必须使用同一个锁,这才是对多个线程同步。
如果把多线程同步了,那跟单线程有什么关系呢?
多线程同步可以把代码块中的代码,只是把共享的数据进行同步,如果不是共享的数据任然按照多线程来处理。
java线程通信
线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。
一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。
以下为一个使用了wait()和notify()实现的线程间通信的共享对象:
public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
注意以下几点:
1、不管是等待线程还是唤醒线程都在同步块里调用wait()和notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出IllegalMonitorStateException异常。
2、一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify()。
3、为了避免丢失信号,必须把它们保存在信号类里。如上面的wasSignalled变量。
4、假唤醒:由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁。
5、不要在字符串常量或全局对象中调用wait()。即上面MonitorObject不能是字符串常量或是全局对象。每一个MyWaitNotify的实例都拥有一个属于自己的监视器对象,而不是在空字符串上调用wait()/notify()。
java中的锁
自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了。
常用的一些锁:
java.util.concurrent.locks.Lock;
java.util.concurrent.locks.ReentrantLock;
java.util.concurrent.locks.ReadWriteLock;
java.util.concurrent.locks.ReentrantReadWriteLock;
一个可重入锁(reentrant lock)的简单实现:
public class Lock {
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
注意的一点:在finally语句中调用unlock()
lock.lock();
try{
//do critical section code, which may throw exception
} finally {
lock.unlock();
}
java中其他同步方法
信号量(Semaphore):java.util.concurrent.Semaphore
阻塞队列(Blocking Queue):java.util.concurrent.BlockingQueue
public class BlockingQueue {
private List queue = new LinkedList();
private int limit = 10;
public BlockingQueue(int limit) {
this.limit = limit;
}
public synchronized void enqueue(Object item) throws InterruptedException {
while (this.queue.size() == this.limit) {
wait();
}
if (this.queue.size() == 0) {
notifyAll();
}
this.queue.add(item);
}
public synchronized Object dequeue() throws InterruptedException {
while (this.queue.size() == 0) {
wait();
}
if (this.queue.size() == this.limit) {
notifyAll();
}
return this.queue.remove(0);
}
}
java中的线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newScheduledThreadPool
创建一个大小无限制的线程池。此线程池支持定时以及周期性执行任务。
newSingleThreadExecutor
创建一个单线程的线程池。此线程池支持定时以及周期性执行任务。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
线程池简单用法:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
}
}