多线程简单入门(Java)(下篇:多线程Java中的使用)
目录
一、创建多线程
二、线程的安全
三、线程的通信
一、创建多线程
在Java中,多线程的创建有4种方式。
方式一:继承于Thread类;
方式二:实现Runnable接口;
方式三:实现Callable()接口;
方式四:使用线程池。
方式一:继承于Thread类。步骤如下:
1、创建一个继承于Thread类的子类;
2、重写Thread类的子类的run()方法;
3、创建一个Thread类的对象;
4、通过对象调用start()方法开启线程。
代码:
//1、创建一个继承于Thread类的子类
class Mythread extends Thread{
//2、重写Thread类的run()
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);//Thread.currentThread().getName()是获取当前线程名的方法
}
}
}
}
public class ThreadTest{
public static void main(String[] args){
//3、创建Thread类的子类的对象
Mythread t1=new Mythread();
//4、通过此对象调用start():启动当前线程,调用当前线程的run()方法
t1.start();
}
}
运行结果(打印了0到100的偶数):
方式二:实现Runnable接口。步骤如下:
(1)、创建一个实现Runnable接口的类;
(2)、(1)中的实现类去实现Runnable接口中的抽象方法run();
3)、创建实现类的对象;
(4)、创建Thread类的对象,并将(3)中的对象作为参数传递到Thread类的构造器中。
(5)、通过Thread类的对象调用start()方法。
代码:
//1、创建一个实现了Runnable接口的类
class Mthread implements Runnable{
//2、实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3、创建实现类的对象
Mthread mthread = new Mthread();
// 4、此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mthread);
//5、通过Thread类的对象调用start()://此时的run()是调用Mthread重写的run()
t1.setName("线程一");//为线程设置名字
t1.start();
//再启动一个线程,也是遍历100以内的偶数
Thread t2 = new Thread(mthread);
t2.setName("线程二");
t2.start();
}
}
运行结果(线程一和线程二都会打印0-100的偶数):
方式一和方式二的比较:
开发中如何选择?优先选择Runnable接口的方式。原因有两个:1、实现Runnable接口的方式突破了Thread类的单继承性的局限性。2、实现Runnable接口的方式更适合处理有多个线程共享数据的情况。
二者的联系:其实Thread类也实现了Runnable接口:public class Thread implements Runnable。
二者的相同点:都需要实现run()方法,run()方法中声明了线程需要执行的逻辑。
下面我们举个例子简单说明一下Thread类中常用的方法。
1、start():启动当前线程并调用当前线程的run()方法。
2、run():通常需要重写Thread类中的此方法,将创建的线程执行的操作声明在此方法中。
3、currentThread():静态方法,返回当前代码执行的线程。
4、getName():获取当前线程的名字。
5、setName():设置当前线程的名字。
6、yield():释放当前CPU的执行权。也叫做线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同的或者更高优先级的线程。若等待队列中没有同优先级的线程,忽略此
方法。
7、join():当某个程序执行流中调用了其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。举个例子:
在线程A中调用线程B的join()方法,此时线程A进入阻塞状态。直到线程B完全执行完以后,线程A才结束阻塞状态。
8、stop():已过时。当执行此方法时,强制结束当前线程。
9、sleep(long millitime):让当前线程睡眠指定的毫秒数。此段时间内,当前线程是阻塞状态。在必要的时候执行sleep()方法会让线程执行地慢一些。
10、isAlive():判断当前线程是否还存活。
11、线程的优先级:
(1)、线程的优先级分为三个等级:最大优先级,最小优先级,默认优先级
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 默认优先级
2)、获取和设置当前线程的优先级
getPriority()
setPriority()
(3)、说明
高优先级的线程要抢占低优先级的线程,但是也只是从概率上这么说。高优先级的线程高概率地被执行,并不意味着”一定是高优先级先执行,结束后再执行低优先级“。
代码:
class HelloThread extends Thread{
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
//sleep()方法的测试:这个时候线程会执行地慢一些。
if (i % 2 == 0) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + i + "," + getPriority());//线程优先级默认是5
//yield()方法的测试:在线程执行到20的时候,必定会交出CPU执行权,让其他线程先执行一次。
// if (i % 20 == 0) {
// Thread.currentThread().yield();//Thread.currentThread()相当于this.
// }
}
}
public HelloThread(String name) {//构造器
super(name);
}
public class ThreadMethodTest {
public static void main(String[] args) throws InterruptedException {
HelloThread h1=new HelloThread("Thread1");//本代码选择在构造器中设置线程的名字。
HelloThread h2=new HelloThread("Thread2");//本代码选择在构造器中设置线程的名字。
//设置名字是主线程所做的事。主线程这里指的是main()主线程 。
//h1.setName("线程一");
//设置线程的优先级。优先级的测试:主线程优先级最低,但是实际中,主线程也可能先执行。尤其说明,优先级高低,只是个概率事件。
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
h2.setPriority(Thread.MAX_PRIORITY);
h2.start();
//为主main()线程命名
Thread.currentThread().setName("主线程");//当前线程就是主线程
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);//设置优先级
for (int i = 0; i <100 ; i++) {
System.out.println(Thread.currentThread().getName()+":"+i+","+Thread.currentThread().getPriority());
//join()方法的测试:在主线程执行到20的时候,被阻塞,线程执行完了,主线程才继续执行。
// if(i==20){
// h1.join();//强行加入线程h1.
// }
}
System.out.println(h1.isAlive());//测试线程是否执行完,执行完就是false.未执行完:true.
}
}
运行结果(每个人可能不一样,我这里是主线程先运行完,线程一线程二才运行的。主线程运行完,线程1的状态为true.):
方式三:实现Callable()接口(JDK5.0新增。该方法获取call()方法的返回值时需要借助FutureTask类)
1、创建一个实现Callable()接口的实现类。
2、实现Call方法,将此线程需要执行的操作声明在call()中。
3、创建实现Callable实现类的对象。
4、创建FutureTask类的对象,并将3中创建的实现类的对象作为参数传递到FutureTask的参数中。
5、将4中FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();
6、获取Call()方法的返回值。
代码:
//1、创建一个实现Callable的实现类
class NumberThread implements Callable<Integer> {
//2、实现Call方法,将此线程需要执行的操作声明在call()中。
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 1; i <=100; i++) {
if(i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;//自动装箱
}
}
public class ThreadNew {
public static void main(String[] args) {
//3、创建Callable接口实现类的对象
NumberThread numberThread=new NumberThread();
//4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中。创建FutureTask的对象。
FutureTask<Integer> futureTask = new FutureTask<Integer>(numberThread);
//5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread 对象,并调用start();
new Thread(futureTask).start();//futureTask实现了Runnable接口
//6、获取call方法的返回值。
Object sum = null;
try {//get()方法的返回值即为futureTask构造器参数Callable实现类重写的call()的返回值。
sum = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("总和为:"+sum);
}
}
运行结果(打印0-100的偶数,并返回0-100的所有偶数的和):
方式二和方式三的比较:
相比于run()方法,Callable()更加强大。call()方法可以有返回值;可以抛出异常;并支持泛型的返回值。
方式四:使用线程池
思路:经常创建和销毁,使用特别大的资源,比如并发情况下的线程,对性能影响很大。如果能提前创建好多个线程放入线程池中,在需要的时候直接获取,使用完再放回池中就很方
便了。这样能避免频繁地创建销毁线程,实现重复利用。
好处:1、能提高响应速度(减少了创建新线程的时间);
2、降低资源消耗(重复利用线程池,不需每次创建);
3、便于线程管理。
1、提供指定线程数量的线程池。
2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。Runnable()用service.execute()调用;Callable接口用service.submit()调用。
3、关闭连接池。
方法:ExecutorService,真正的线程池API.常见的子类:ThreadPoolExecutor。
void execute(Runnable command):执行任务/命令,没有返回值。一般用来执行Runnable。
void shutdown:关闭连接池。
Executors:工具类,线程池的工厂类。用于创建并返回不同类型的线程池。
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程的线程池。
代码:
class NumberThreadPool implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
}
class NumberThreadPool1 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i%2==1){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性
ThreadPoolExecutor service1=(ThreadPoolExecutor) service;//强转
System.out.println(service.getClass());
service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。
//service.submit(new NumberThreadPool());//适合使用于Callable()
service.execute(new NumberThreadPool());//适合使用于Runnable()//打印偶数
service.execute(new NumberThreadPool1());//适合使用Runnable()//打印奇数
//关闭连接池
service.shutdown();
}
}
运行结果:
二、线程的同步
先看一个窗口售票的例子:创建三个窗口卖票。总票数为100张
代码如下:
class Window extends Thread{
private static int ticket=100;//三个线程共享同一个ticket——总票数,
// 三个线程卖100张票
@Override
public void run() {
super.run();
while(true){
if(ticket>0){
System.out.println(getName()+":卖票,票号为"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
运行结果:出现了重票,存在线程安全问题。
分析一下,为何存在线程安全问题?---因为存在共享数据ticket,三个线程操作一个变量。
1、问题一:出现了重票错票问题。
2、问题二:出现的原因:当某个线程来操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作了车票
3、问题三:如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到线程A操作完以后,其他线程才可以操作。即使线程A出现了阻塞也不能被改变。
在Java中,我们通过同步机制来解决线程的安全问题。有如下三种方式。分别是同步代码块,同步方法,LOCK锁。有了同步的方式,操作代码时,只能有一个线程参与,其他线程等
待。相当于是一个单线程的过程。同步解决了线程的安全问题。
方式一:同步代码块 ,使用synchronized关键字。
synchronized(同步监视器){
//需要被同步的代码
}
说明:1、操作共享数据的代码,即为需要被同步的代码。不能包含多了,也不能包含少了。
2、共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。
3、同步监视器:俗称:锁。任何一个类的对象都可以来充当这个锁。
注意:多个线程必须共用一把锁。在实现Runnable时,可以考虑用this充当这个锁。
代码:
//继承类的方法,同步监视器****慎用this
class Window2 extends Thread {
private static int ticket = 100;//三个线程共享同一个ticket——总票数,
// 三个线程卖100张票
private static Object obj = new Object();//保证三个共享同一个数据
@Override
public void run() {
super.run();
while (true) {//Class class =Window2.class->
//synchronized (Window2.class) {
synchronized (obj) {//此时不能用this,因为this代表着t1,t2,t3.此时锁不唯一。
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1=new Window2();
Window2 t2=new Window2();
Window2 t3=new Window2();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
//实现Runnable接口的方法
class Window1 implements Runnable {
private int ticket = 100;//不用加static
Object obj = new Object();//obj就是一个锁。
@Override
public void run() {
//Object obj = new Object();//obj只能是一个锁。唯一性
while (true) {
synchronized (this) {//此时this代表的就是唯一的window1对象。
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();//共用一个window1对象
Thread t1 = new Thread(w);//w作为参数传递给线程Thread
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
运行结果(线程安全):
方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的(使用synchronized关键字)。
关于同步方法的总结:
1、同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。
2、非静态的同步方法,同步监视器是:this。
静态的同步方法,同步监视器是:当前类本身。
代码:
//使用同步方法来处理继承Thread类的方式中线程安全问题
class Window4 extends Thread {
private static int ticket = 100;//三个线程共享同一个ticket——总票数,
@Override
public void run() {
super.run();
while (true) {
show();
}
}
private static synchronized void show(){//同步监视器:t1,t2,t2
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1=new Window4();
Window4 t2=new Window4();
Window4 t3=new Window4();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
//使用同步方法解决实现Runnable接口的线程安全问题
class Window3 implements Runnable {
private int ticket = 100;//不用加static
@Override
public void run() {
while (true) {
show();
}
}
//同步方法
private synchronized void show(){
//synchronized(this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();//共用一个window1对象
Thread t1 = new Thread(w);//w作为参数传递给线程Thread
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
方式三:LOCK锁(JDK5.0新增)
方式一和方式二都使用到了synchronized关键字。那么,synchronized与LOCK有何异同?
相同:二者都可以解决线程安全的问题。
不同之处:synchronized机制在执行完相应的代码逻辑以后,自动地释放同步监视器。
另外需要注意的是LOCK需要手动地启动同步:lock(),同时结束也是需要手动的实现:unlock()。
实现LOCK锁步骤:
1、创建锁的对象;
2、上锁:调用lock()方法;
3、解锁:调用unlock()方法。
代码:
class Window implements Runnable{
private int ticket=100;
//实例化lock
private ReentrantLock lock=new ReentrantLock(true);//1、创建锁的对象
@Override
public void run() {
while(true){
try{
//2、调用lock方法
lock.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票:"+"票号为"+ticket);
ticket--;
}else{
break;
}
}finally{
//3、解锁的方法:unlock
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w=new Window();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
三、线程通信
下面以一个简单的例子来理解一下线程通信的方法。
//两个线程交替打印1到100
class Number implements Runnable{
private int number =1;//共享数据,线程安全问题
@Override
public void run() {
while(true){
synchronized (this) {
//唤醒
notify();//notifyAll():唤醒多个。
if(number<=100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
//使得调用如下wait()方法的线程进入阻塞状态
try {
wait();//wait完了以后会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2= new Thread(number);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
运行结果:
线程通信有三个方法:wait(),notify(),notifyAll()。
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。wait()与sleep()不同,在被运用同步代码块中时,sleep()休眠以后,不会释放锁。但是wait()执
行完后会释放锁。
notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程,就会唤醒优先级高的线程。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1、wait(),notify(),notifyAll()只能出现在同步块或同步方法中。
2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现MonitorStateException异常。
3、三个方法定义在Object类中。方便任何一个对象充当同步监视器的调用。
线程通信的应用:生产者消费者问题
分析:是多线程问题。生产者线程,消费者线程。
是否有共享数据?是,店员或产品的数量。
如何解决线程的安全问题?同步机制,三种方式。
代码:
class Clerk {
private int productCount = 0;//生产的产品数量
//以下两个方法需要同步,否则存在线程安全问题
//生产产品(同步方法)
public synchronized void produceProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumerProduct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName() + "开始消费第" + productCount + "个产品");
productCount--;
notify();
} else {//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + ":开始生产产品.......");
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();//生产
}
}
}
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk=clerk;
}
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + ":开始消费产品.......");
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumerProduct();//消费
}
}
}
public class ProduceTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2= new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
运行结果: